spark机器学习笔记:(三)用Spark Python构建推荐系统

声明:版权所有,转载请联系作者并注明出处  http://blog.csdn.net/u013719780?viewmode=contents


博主简介:风雪夜归子(英文名:Allen),机器学习算法攻城狮,喜爱钻研Meachine Learning的黑科技,对Deep Learning和Artificial Intelligence充满兴趣,经常关注Kaggle数据挖掘竞赛平台,对数据、Machine Learning和Artificial Intelligence有兴趣的童鞋可以一起探讨哦,个人CSDN博客:http://blog.csdn.net/u013719780?viewmode=contents


上一篇博文详细介绍了如何使用Spark Python进行数据处理和特征提取,本系列从本文开始,将陆续介绍用Spark Python对机器学习模型进行详细的探讨。

推荐引擎或许是最为大众所知的一种机器学习模型。人们或许并不知道它确切是什么,但在使用AmazonNetflixYouTubeTwitterLinkedInFacebook这些流行站点的时候,可能已经接触过了。推荐是这些网站背后的核心组件之一,有时还是一个重要的收入来源。

推荐引擎背后的想法是预测人们可能喜好的物品并通过探寻物品之间的联系来辅助这个过程。从这点上来说,它和同样也做预测的搜索引擎互补。但与搜索引擎不同,推荐引擎试图向人们呈现的相关内容并不一定就是人们所搜索的,其返回的某些结果甚至人们都没听说过。

推荐引擎很适合如下两类常见场景(两者可兼有)。

可选项众多:可选的物品越多,用户就越难找到想要的物品。如果用户知道他们想要什么,那搜索能有所帮助。然而最适合的物品往往并不为用户所事先知道。这时,通过向用户推荐相关物品,其中某些可能用户事先不知道,将能帮助他们发现新物品。

偏个人喜好:当人们主要根据个人喜好来选择物品时,推荐引擎能利用集体智慧,根据其他有类似喜好用户的信息来帮助他们发现所需物品。



1 推荐模型的分类


推荐系统的研究已经相当广泛,也存在很多设计方法。最为流行的两种方法是基于内容的过滤和协同过滤。另外,排名模型等近期也受到不少关注。实践中的方案很多是综合性的,它们将多种方法的元素合并到一个模型中或是进行组合。

1.1 基于内容的过滤

基于内容的过滤利用物品的内容或是属性信息以及某些相似度定义,来求出与该物品类似的物品。这些属性值通常是文本内容(比如标题、名称、标签及该物品的其他元数据)。对多媒体来说,可能还涉及从音频或视频中提取的其他属性。

类似地,对用户的推荐可以根据用户的属性或是描述得出,之后再通过相同的相似度定义来与物品属性做匹配。比如,用户可以表示为他所接触过的各物品属性的综合。该表示可作为该用户的一种描述。之后可以用它来与物品的属性进行比较以找出符合用户描述的物品。

1.2协同过滤

协同过滤是一种借助众包智慧的途径。它利用大量已有的用户偏好来估计用户对其未接触过的物品的喜好程度。其内在思想是相似度的定义。

在基于用户的方法的中,如果两个用户表现出相似的偏好(即对相同物品的偏好大体相同),那就认为他们的兴趣类似。要对他们中的一个用户推荐一个未知物品,便可选取若干与其类似的用户并根据他们的喜好计算出对各个物品的综合得分,再以得分来推荐物品。其整体的逻辑是,如果其他用户也偏好某些物品,那这些物品很可能值得推荐。

同样也可以借助基于物品的方法来做推荐。这种方法通常根据现有用户对物品的偏好或是评级情况,来计算物品之间的某种相似度。这时,相似用户评级相同的那些物品会被认为更相近。一旦有了物品之间的相似度,便可用用户接触过的物品来表示这个用户,然后找出和这些已知物品相似的那些物品,并将这些物品推荐给用户。同样,与已有物品相似的物品被用来生成一个综合得分,而该得分用于评估未知物品的相似度。

基于用户或物品的方法的得分取决于若干用户或是物品之间依据相似度所构成的集合(即邻居),故它们也常被称为最近邻模型。

最后,也存在不少基于模型的方法是对“用户物品”偏好建模。这样,对未知“用户物品”组合上应用该模型便可得出新的偏好。

1.3 矩阵分解

Spark推荐模型库当前只包含基于矩阵分解(matrix factorization)的实现,由此我们也将重点关注这类模型。它们有吸引人的地方。首先,这些模型在协同过滤中的表现十分出色。而在Netflix Prize等知名比赛中的表现也很拔尖。


1.3.1 显式矩阵分解

当要处理的那些数据是由用户所提供的自身的偏好数据,这些数据被称作显式偏好数据。这类数据包括如物品评级、赞、喜欢等用户对物品的评价。

这些数据可以转换为以用户为行、物品为列的二维矩阵。矩阵的每一个数据表示某个用户对特定物品的偏好。大部分情况下单个用户只会和少部分物品接触,所以该矩阵只有少部分数据非零(即该矩阵很稀疏)。

举个例子,假设我们有如下用户对电影的评级数据

user ratings 数据:

Tom, Star Wars, 5
Jane, Titanic, 4
Bill, Batman, 3
Jane, Star Wars, 2
Bill, Titanic, 3

以user为行,movie为列构造对应rating matrix:



MF就是一种直接建模user-item矩阵的方法,利用两个低维度的小矩阵的乘积来表示,属于一种降维的技术。

如果我们有U个用户,I个items,若不经过MF处理,它看来会使这样的:

                                                                                                                        

值得注意的是,上述评级矩阵通常很稀疏,但因子矩阵却是稠密的,经过MF处理后,表示为两个维度较小的因子矩阵相乘,如图所示:



这类模型试图发现对应“用户物品”矩阵内在行为结构的隐含特征(这里表示为因子矩阵),所以也把它们称为隐特征模型。隐含特征或因子不能直接解释,但它可能表示了某些含义,比如对电影的某个导演、种类、风格或某些演员的偏好。

由于是对“用户物品”矩阵直接建模,用这些模型进行预测也相对直接:要计算给定用户对某个物品的预计评级,就从用户因子矩阵和物品因子矩阵分别选取相应的行(用户因子向量)与列(物品因子向量),然后计算两者的点积即可,如下图所示


而对于物品之间相似度的计算,可以用最近邻模型中用到的相似度衡量方法。不同的是,这里可以直接利用物品因子向量,将相似度计算转换为对两物品因子向量之间相似度的计算,如下图所示:


因子分解类模型的好处在于,一旦建立了模型,对推荐的求解便相对容易。但也有弊端,即当用户和物品的数量很多时,其对应的物品或是用户的因子向量可能达到数以百万计。这将在存储和计算能力上带来挑战。另一个好处是,这类模型的表现通常都很出色。

因子分解类模型也存在某些弱点。相比最近邻模型,这类模型在理解和可解释性上难度都有所增加。另外,其模型训练阶段的计算量也很大。



1.3.2隐式矩阵分解


上面针对的是评级之类的显式偏好数据,但能收集到的偏好数据里也会包含大量的隐式反馈数据。在这类数据中,用户对物品的偏好不会直接给出,而是隐含在用户与物品的交互之中。二元数据(比如用户是否观看了某个电影或是否购买了某个商品)和计数数据(比如用户观看某电影的次数)便是这类数据。

处理隐式数据的方法相当多。MLlib实现了一个特定方法,它将输入的评级数据视为两个矩阵:一个二元偏好矩阵P以及一个信心权重矩阵C

举例来说,假设之前提到的“用户电影”评级实际上是各用户观看电影的次数,那上述两个矩阵会类似下图所示。其中,矩阵P表示用户是否看过某些电影,而矩阵C则以观看的次数来表示信心权重。一般来说,某个用户观看某个电影的次数越多,那我们对该用户的确喜欢该电影的信心也就越强。


隐式模型仍然会创建一个用户因子矩阵和一个物品因子矩阵。但是,模型所求解的是偏好矩阵而非评级矩阵的近似。类似地,此时用户因子向量和物品因子向量的点积所得到的分数也不再是一个对评级的估值,而是对某个用户对某一物品偏好的估值(该值的取值虽并不严格地处于01之间,但十分趋近于这个区间)。



1.3.3 最小二乘法


最小二乘法(Alternating Least Squares,ALS)是一种求解矩阵分解问题的最优化方法。它功能强大、效果理想而且被证明相对容易并行化。这使得它很适合如Spark这样的平台。

ALS的实现原理是迭代式求解一系列最小二乘回归问题。在每一次迭代时,固定用户因子矩阵或是物品因子矩阵中的一个,然后用固定的这个矩阵以及评级数据来更新另一个矩阵。之后,被更新的矩阵被固定住,再更新另外一个矩阵。如此迭代,直到模型收敛(或是迭代了预设好的次数)。



2 提取有效特征


这里,我们将采用显式评级数据,而不使用其他用户或物品的元数据以及“用户物品”交互数据。这样,所需的输入数据就只需包括每个评级对应的用户ID、影片ID和具体的星级。本文中使用显式数据也就是用户对movie的rating信息,这个数据来源于网络上的MovieLens标准数据集



rawData = sc.textFile("/Users/youwei.tan/ml-100k/u.data")
print rawData.first()
rawRatings = rawData.map(lambda x: x.split('\t'))
rawRatings.take(5)
#数据分别是userId,itemId,rating和timestamp

输出结果:

196	242	3	881250949
[[u'196', u'242', u'3', u'881250949'],
 [u'186', u'302', u'3', u'891717742'],
 [u'22', u'377', u'1', u'878887116'],
 [u'244', u'51', u'2', u'880606923'],
 [u'166', u'346', u'1', u'886397596']]

导入RatingALS模块,用于后面建模

from pyspark.mllib.recommendation import Rating, ALS
ratings = rawRatings.map(lambda x: Rating(int(x[0]),int(x[1]),float(x[2])))
print ratings.take(5)

输出结果:

[Rating(user=196, product=242, rating=3.0), 
 Rating(user=186, product=302, rating=3.0), 
 Rating(user=22, product=377, rating=1.0), 
 Rating(user=244, product=51, rating=2.0), 
 Rating(user=166, product=346, rating=1.0)]


3 训练推荐模型


现在可以开始训练模型了,所需的其他参数有以下几个。

rank:对应ALS模型中的因子个数,也就是在低阶近似矩阵中的隐含特征个数。因子个数一般越多越好。但它也会直接影响模型训练和保存时所需的内存开销,尤其是在用户和物品很多的时候。因此实践中该参数常作为训练效果与系统开销之间的调节参数。通常,其合理取值为10200

iterations:对应运行时的迭代次数。ALS能确保每次迭代都能降低评级矩阵的重建误差,但一般经少数次迭代后ALS模型便已能收敛为一个比较合理的好模型。这样,大部分情况下都没必要迭代太多次(10次左右一般就挺好)。

lambda:该参数控制模型的正则化过程,从而控制模型的过拟合情况。其值越高,正则化越严厉。该参数的赋值与实际数据的大小、特征和稀疏程度有关。和其他的机器学习模型一样,正则参数应该通过用非样本的测试数据进行交叉验证来调整。

作为示例,这里将使用的rankiterationslambda参数的值分别为50100.01:

model = ALS.train(ratings, 50, 10, 0.01)
userFeatures = model.userFeatures()
print userFeatures.take(2)

输出结果:

[(4, array('d', [-0.4886532723903656, -0.5069043636322021, -0.25953221321105957, 0.28663870692253113, 
-0.23391617834568024, -0.1597181111574173, -0.6053574085235596, -0.05088397115468979, -0.13468056917190552, 
-0.06684212386608124, -0.1647985279560089, -0.3268186151981354, 0.04303080216050148, -0.37185072898864746, 
-0.07605815678834915, -0.18150471150875092, 0.4730634391307831, -0.17112521827220917, 0.011302260681986809, 
0.025912854820489883, 0.22327645123004913, -0.553532063961029, -0.19743777811527252, -0.06508021801710129, 
0.11410114169120789, -0.2178303301334381, -0.12165536731481552, 0.1517478972673416, -0.41739341616630554, 
-0.3790307939052582, 0.09140809625387192, 0.438218355178833, -0.40334436297416687, -0.26500797271728516, 
-0.17151066660881042, -0.056731898337602615, 0.014920198358595371, -0.4036138951778412, -0.503244161605835, 
-0.35094475746154785, -0.2583906650543213, 0.15083833038806915, -0.02699241414666176, -0.5867881774902344, 
0.6203442215919495, -0.5354999303817749, 0.49261313676834106, 0.36879995465278625, 0.217197448015213, 
0.2137724757194519])), 
(8, array('d', [-0.3435080349445343, -0.23504899442195892, 0.01265001017600298, 0.37398409843444824, 
-0.12563304603099823, 0.35209083557128906, 0.14092524349689484, -0.03852889686822891, 0.35797831416130066, 
-0.21024121344089508, -0.4891291558742523, 0.13990764319896698, -0.2572784125804901, 0.7792724370956421, 
0.2034142017364502, 0.5507923364639282, -0.047871965914964676, 0.24563290178775787, 0.14227859675884247, 
-0.3111373484134674, 0.6031113266944885, -0.5081701874732971, 0.39406323432922363, 0.17398110032081604, 
0.7309723496437073, 0.38500913977622986, -0.39698582887649536, 0.3925837576389313, -0.14110979437828064, 
-0.3914717435836792, 0.36928772926330566, 0.6285825967788696, -0.574873149394989, -0.4255753457546234, 
-0.14399465918540955, -0.11449692398309708, 0.32894405722618103, 0.17887713015079498, 0.23037177324295044, 
-0.17776504158973694, -0.17060403525829315, -0.28235891461372375, -0.33164700865745544, -0.05743246525526047, 
0.0831192284822464, -0.007868407294154167, 0.0913839191198349, 0.6946220993995667, 0.3273734152317047, 
-0.22331389784812927]))]

注意,MLlibALS的实现里所用的操作都是延迟性的转换操作。所以,只在当用户因子或物品因子结果RDD调用了执行操作时,实际的计算才会发生。要强制计算

发生,则可调用Spark的执行操作,如count。

print model.userFeatures().count()
print model.productFeatures().count()

输出结果:

943
1682

使用隐式反馈数据训练模型

MLlib中标准的矩阵分解模型用于显式评级数据的处理。若要处理隐式数据,则可使用trainImplicit函数。其调用方式和标准的train模式类似,但多了一个可设置的alpha参数也是一个正则化参数,lambda应通过测试和交叉验证法来设置

alpha 参数指定了信心权重所应达到的基准线。该值越高则所训练出的模型越认为用户与他所没评级过的电影之间没有相关性。





4 使用推荐模型


4.1用户推荐


有了训练好的模型后便可用它来做预测。预测通常有两种:为某个用户推荐物品,或找出与某个物品相关或相似的其他物品。

MovieLens 100k数据集生成电影推荐

MLlib的推荐模型基于矩阵分解,因此可用模型所求得的因子矩阵来计算用户对物品的预计评级。下面只针对利用MovieLens中显式数据做推荐的情形,使用隐式模型时的方法与之相同。

print len(userFeatures.first()[1])
predictRating = model.predict(789,123)
print predictRating

输出结果:

50
3.18324449604

可以看到,该模型预测用户789对电影123的评级为3.18

predict函数同样可以以(user, item)ID对类型的RDD对象为输入,这时它将为每一对都生成相应的预测得分。我们可以借助这个函数来同时为多个用户和物品

进行预测。

要为某个用户生成K推荐物品,可借助MatrixFactorizationModel所提供的recommendProducts函数来实现。该函数需两个输入参数:usernum。其中user

是用户ID,而num是要推荐的物品个数。

返回值为预测得分最高的前num个物品。这些物品的序列按得分排序。该得分为相应的用户因子向量和各个物品因子向量的点积。

现在,计算给用户789推荐的前10个物品,代码如下:


topKRecs = model.recommendProducts(789,10)
print '给用户userId推荐其喜欢的item:'
for rec in topKRecs:
    print rec

输出结果:

给用户userId推荐其喜欢的item:
Rating(user=789, product=48, rating=5.572495011959803)
Rating(user=789, product=56, rating=5.510520005211698)
Rating(user=789, product=346, rating=5.295860840700527)
Rating(user=789, product=192, rating=5.08681379224634)
Rating(user=789, product=92, rating=5.01867372270688)
Rating(user=789, product=526, rating=5.016016542149785)
Rating(user=789, product=182, rating=4.9996960801921695)
Rating(user=789, product=179, rating=4.9877606036865005)
Rating(user=789, product=100, rating=4.971601345359906)
Rating(user=789, product=9, rating=4.969371624015052)

检验推荐内容

要直观地检验推荐的效果,可以简单比对下用户所评级过的电影和被推荐的那些电影

下面查看用户789评级过的电影,代码如下:

moviesForUser = ratings.groupBy(lambda x : x.user).mapValues(list).lookup(789)
#print ratings.groupBy(lambda x : x.user).mapValues(list).lookup(789)
#print ratings.groupBy(lambda x : x.user).map(lambda (x,y): (x, list(y))).lookup(789)
#print moviesForUser

print '用户对%d部电影进行了评级'%len(moviesForUser[0])
print '源数据中用户(userId=789)喜欢的电影(item):'
for i in sorted(moviesForUser[0],key=lambda x : x.rating,reverse=True):
    print i.product

输出结果:
用户对33部电影进行了评级
源数据中用户(userId=789)喜欢的电影(item):
127
475
9
50
150
276
129
100
741
1012
93
293
181
1008
508
1007
124
1161
294
1
284
1017
111
742
248
249
591
288
762
628
137
151
286


movies = sc.textFile("/Users/youwei.tan/ml-100k/u.item")
titles = movies.map(lambda line: (int(line.split('|')[0]),line.split('|')[1])).collectAsMap()
print titles[1]
输出结果:

Toy Story (1995)


for i,rec in enumerate(topKRecs):
    print 'rank:'+str(i)+' '+str(titles[rec.product])+':'+str(rec.rating)


输出结果:
rank:0 Hoop Dreams (1994):5.57249501196
rank:1 Pulp Fiction (1994):5.51052000521
rank:2 Jackie Brown (1997):5.2958608407
rank:3 Raging Bull (1980):5.08681379225
rank:4 True Romance (1993):5.01867372271
rank:5 Ben-Hur (1959):5.01601654215
rank:6 GoodFellas (1990):4.99969608019
rank:7 Clockwork Orange, A (1971):4.98776060369
rank:8 Fargo (1996):4.97160134536
rank:9 Dead Man Walking (1995):4.96937162402



使用recommendProducts来为用户789推荐top10的items,其items顺序为降序。MoviesForUser是从ratings数据中找出的用户789rating最高的数据,仔细看下发现数据和我们的ratings里面找出的数据貌似一个都没有相同的,那么是不是说明我们的算法不给力呢?这个可不一定,想想看,如果推荐系统只是推荐给你看过的电影,那么它一定是一个失败的,并且完全对系统的kpi数据无提升作用,前面提到,MF的实质是通过latent feature去找到与用户过去偏好高的有某些隐性相同特征的电影(这些由整体用户的集体智慧得到),比如可能是某一类型的电影、又或者相同的演员等等,所以这里不能说明推荐系统不给力,但是确实也很难具有解释性。



4.2物品推荐


物品推荐是为回答如下问题:给定一个物品,有哪些物品与它最相似?这里,相似的确切定义取决于所使用的模型。大多数情况下,相似度是通过某种方式比较表示两个物品的向量而得到的。常见的相似度衡量方法包括皮尔森相关系数(Pearson correlation)、针对实数向量的余弦相似度(cosine similarity)和针对二元向量的杰卡德相似系数(Jaccard similarity)。


MovieLens 100k数据集生成相似电影

MatrixFactorizationModel当前的API不能直接支持物品之间相似度的计算。所以我们要自己实现。这里我们使用余弦相似度来衡量相似度。

import numpy as np
def cosineSImilarity(x,y):
    return np.dot(x,y)/(np.linalg.norm(x)*np.linalg.norm(y))
testx = np.array([1.0,2.0,3.0])
print cosineSImilarity(testx,testx)

输出结果:1.0

下面以 itemId = 567为例,计算所有与物品itemId = 567的的相似度,并取其相似度最高的前十个,代码如下。

itemId = 567
itemFactor = model.productFeatures().lookup(itemId)[0]
print itemFactor

#print model.productFeatures().collect()
#print model.productFeatures().take(1)
sims = model.productFeatures().map(lambda (id,factor):(id,cosineSImilarity(np.array(factor),
             np.array(itemFactor))))

sims.sortBy(lambda (x,y):y,ascending=False).take(10)


输出结果:
array('d', 
[-0.14139963686466217, 0.6053799390792847, -0.4522729516029358, 0.962456226348877, 0.2535054385662079, -0.4816886782646179, 
-0.7732354402542114, 0.7280293703079224, -0.018588555976748466, 0.7229052782058716, 0.44682827591896057, 0.315813809633255, 
-0.4070151746273041, -0.08554915338754654, -0.2362387776374817, -0.4164128005504608, -0.2916576862335205, 0.8387507200241089, 
-0.10055144876241684, -0.5498601198196411, -0.11159903556108475, -1.4316672086715698, -1.1710675954818726, -0.5741096138954163, 
0.30021053552627563, -0.5658889412879944, -0.2256290316581726, 1.0022759437561035, 0.4253554940223694, -0.03920847177505493, 
-0.2296391874551773, 0.8324244022369385, 0.1434708833694458, 0.1821751743555069, -0.709479570388794, -0.7556734085083008, 
0.9100260734558105, -0.4104197025299072, 0.22313174605369568, -0.5352709293365479, -0.07432051002979279, 0.42420247197151184, 
-0.10177311301231384, -0.09117455780506134, -0.5774176120758057, 0.13856376707553864, 0.7275233268737793, -0.12974925339221954, 
0.7839331030845642, 0.3163391649723053])
[(567, 0.99999999999999989),
 (563, 0.7309392456908036),
 (1376, 0.72832388806473258),
 (413, 0.71275894340962187),
 (1291, 0.70333653584495592),
 (436, 0.70019468584044475),
 (96, 0.69824883927286607),
 (746, 0.69715419740193796),
 (331, 0.69497368094311607),
 (408, 0.69223520237106451)]




5 推荐模型效果的评估



如何知道训练出来的模型是一个好模型?这就需要某种方式来评估它的预测效果。评估指标(evaluation metric)指那些衡量模型预测能力或准确度的方法。它们有些直接度量模型的预测目标变量的好坏(比如均方差),有些则关注模型对那些其并未针对性优化过但又十分接近真实应用场景数据的预测能力(比如平均准确率)。

评估指标提供了同一模型在不同参数下,又或是不同模型之间进行比较的标准方法。通过这些指标,人们可以从待选的模型中找出表现最好的那个模型。本博文主要介绍推荐系统和协同过滤模型里常用的两个指标:均方差以及K值平均准确率。



5.1 均方差


均方差(Mean Squared Error,MSE)直接衡量“用户物品”评级矩阵的重建误差。它也是一些模型里所采用的最小化目标函数,特别是许多矩阵分解类方法,比如ALS。因此,它常用于显式评级的情形。

它的定义为各平方误差的和与总数目的商。其中平方误差是指预测到的评级与真实评级的差值的平方。下面以用户789为例做讲解。现在从之前计算的moviesForUser这个Ratings集合里找出该用户的第一个评级:


actual = moviesForUser[0][0]
actualRating = actual.rating
print '用户789对电影1012的实际评级',actualRating
predictedRating = model.predict(789, actual.product)
print '用户789对电影1012的预测评级',predictedRating
squaredError = np.power(actualRating-predictedRating,2)
print '实际评级与预测评级的MSE',squaredError


输出结果:
用户789对电影1012的实际评级 4.0
用户789对电影1012的预测评级 3.98669257292
实际评级与预测评级的MSE 0.000177087615556

moviesForUser


输出结果:
[[Rating(user=789, product=1012, rating=4.0),
  Rating(user=789, product=127, rating=5.0),
  Rating(user=789, product=475, rating=5.0),
  Rating(user=789, product=93, rating=4.0),
  Rating(user=789, product=1161, rating=3.0),
  Rating(user=789, product=286, rating=1.0),
  Rating(user=789, product=293, rating=4.0),
  Rating(user=789, product=9, rating=5.0),
  Rating(user=789, product=50, rating=5.0),
  Rating(user=789, product=294, rating=3.0),
  Rating(user=789, product=181, rating=4.0),
  Rating(user=789, product=1, rating=3.0),
  Rating(user=789, product=1008, rating=4.0),
  Rating(user=789, product=508, rating=4.0),
  Rating(user=789, product=284, rating=3.0),
  Rating(user=789, product=1017, rating=3.0),
  Rating(user=789, product=137, rating=2.0),
  Rating(user=789, product=111, rating=3.0),
  Rating(user=789, product=742, rating=3.0),
  Rating(user=789, product=248, rating=3.0),
  Rating(user=789, product=249, rating=3.0),
  Rating(user=789, product=1007, rating=4.0),
  Rating(user=789, product=591, rating=3.0),
  Rating(user=789, product=150, rating=5.0),
  Rating(user=789, product=276, rating=5.0),
  Rating(user=789, product=151, rating=2.0),
  Rating(user=789, product=129, rating=5.0),
  Rating(user=789, product=100, rating=5.0),
  Rating(user=789, product=741, rating=5.0),
  Rating(user=789, product=288, rating=3.0),
  Rating(user=789, product=762, rating=3.0),
  Rating(user=789, product=628, rating=3.0),
  Rating(user=789, product=124, rating=4.0)]]



要计算整个数据集上的MSE,需要对每一条(user, movie, actual rating, predictedrating)记录都计算该平均误差,然后求和,再除以总的评级次数。具体实现如下:

userProducts = ratings.map(lambda rating:(rating.user,rating.product))
print '实际的评分:',userProducts.take(5)

#预测所有用户对电影的相应评分
print model.predictAll(userProducts).collect()[0]
predictions = model.predictAll(userProducts).map(lambda rating:((rating.user,rating.product), rating.rating))
print '预测的评分:',predictions.take(5)

ratingsAndPredictions = ratings.map(lambda rating:((rating.user,rating.product),rating.rating)).join(predictions)
print '组合预测的评分和实际的评分:',ratingsAndPredictions.take(5)

MSE = ratingsAndPredictions.map(lambda ((x,y),(m,n)):np.power(m-n,2)).reduce(lambda x,y:x+y)/ratingsAndPredictions.count() 
print '模型的均方误差:',MSE 
print '模型的均方根误差:',np.sqrt(MSE)

输出结果:

实际的评分: [(196, 242), (186, 302), (22, 377), (244, 51), (166, 346)]
Rating(user=316, product=1084, rating=4.01303094210043)
预测的评分: [((316, 1084), 4.01303094210043), ((504, 1084), 3.8890732638837306), ((424, 1084), 4.99014452689434), 
((541, 1084), 4.087339438976559), ((181, 1084), 2.0708407464846563)]
组合预测的评分和实际的评分: [((711, 707), (5.0, 5.068769168364991)), ((650, 622), (3.0, 3.078863587357608)), 
((472, 584), (1.0, 1.5594002961174487)), ((752, 316), (3.0, 2.822048986057712)), ((18, 428), (3.0, 3.190997207172113))]
模型的均方误差: 0.0851271357577
模型的均方根误差: 0.291765549299



5.2 K值平均准确率


K值平均准确率(MAP)的意思是整个数据集上的K值平均准确率(Average Precision at Kmetric,MAP)的均值。APK是信息检索中常用的一个指标。它用于衡量针对某个查询所返回的“前K个”文档的平均相关性。对于每次查询,我们会将结果中的前K个与实际相关的文档进行比较。

MAP指标计算时,结果中文档的排名十分重要。如果结果中文档的实际相关性越高且排名也更靠前,那APK分值也就越高。由此,它也很适合评估推荐的好坏。因为推荐系统也会计算“前K个”推荐物,然后呈现给用户。如果在预测结果中得分更高(在推荐列表中排名也更靠前)的物品实际上也与用户更相关,那自然这个模型就更好。MAP和其他基于排名的指标同样也更适合评估隐式数据集上的推荐。这里用MSE相对就不那么合适。



当用MAP来做评估推荐模型时,每一个用户相当于一个查询,而每一个“前K个”推荐物组成的集合则相当于一个查到的文档结果集合。用户对电影的实际评级便对应着文档的实际相关性。这样,APK所试图衡量的是模型对用户感兴趣和会去接触的物品的预测能力,MAP的Python脚本如下:

def avgPrecisionK(actual, predicted, k): 
    if len(predicted) > k:
        predK = predicted[:k]
    else:
        predK = predicted
    score = 0.0
    numHits = 0.0
    for i,p in enumerate(predK):
        if p in actual and p not in predK:
            numHits = numHits + 1
            score = score + numHits/(i+1)
    if not actual:
        return 1.0
    else:
        return score/min(len(actual),k)


下面计算模型对用户789推荐的MAP。首先提取出用户实际评级过的电影的ID预测电影ID:

actualMovies = [rating.product for rating in moviesForUser[0]]
predictMovies = [rating.product for rating in topKRecs]
print '实际的电影:',actualMovies
print '预测的电影:',predictMovies

输出结果:

实际的电影: [1012, 127, 475, 93, 1161, 286, 293, 9, 50, 294, 181, 1, 1008, 508, 284, 1017, 137, 111, 742, 248, 249, 1007, 591, 
150, 276, 151, 129, 100, 741, 288, 762, 628, 124]
预测的电影: [48, 56, 346, 192, 92, 526, 182, 179, 100, 9]


计算MAP值

MAP10 = avgPrecisionK(actualMovies,predictMovies,10)
print MAP10

输出结果:

0.0


这里,APK的得分为0,这表明该模型在为该用户做相关电影预测上的表现并不理想。

全局MAPK的求解要计算对每一个用户的APK得分,再求其平均。这就要为每一个用户都生成相应的推荐列表。针对大规模数据处理时,这并不容易,但我们可以通过Spark将该计算分布式进行。不过,这就会有一个限制,即每个工作节点都要有完整的物品因子矩阵。这样它们才能独立地计算某个物品向量与其他所有物品向量之间的相关性。然而当物品数量众多时,单个节点的内存可能保存不下这个矩阵。此时,这个限制也就成了问题。

下面看一看如何求解。首先,取回物品因子向量并用它来构建一个DoubleMatrix对象

itemFactors = model.productFeatures().map(lambda (id,factor):factor).collect()
itemMatrix = np.array(itemFactors)
print itemMatrix
print itemMatrix.shape

输出结果:

[ 0.02446744 -1.29959726 -0.41932318 ...,  0.81818616  0.18477531
   0.24459557]
 [-0.24553037 -1.99444282 -0.25264686 ...,  0.82401907  0.8064388
  -0.30488601]
 [ 0.0659191  -1.3440671  -1.0006181  ...,  0.98762459  0.80993211
  -0.1881171 ]
 ..., 
 [ 0.03007785 -0.199938   -0.06636383 ..., -0.0149461   0.06697606
   0.00769717]
 [-0.17347991 -0.24106073 -0.42270574 ...,  0.33844769 -0.01864041
   0.06676605]
 [ 0.00804477 -0.12564704 -0.09706354 ..., -0.0166881  -0.18410273
  -0.16378592]]
(1682, 50)

这说明itemMatrix的行列数分别为168250。这正常,因为电影数目和因子维数分别就是这么多。接下来,我们将该矩阵以一个广播变量的方式分发出去,以便每个工作节点都能访问到:

#便于后面进行map计算的时候使所有map都有效,这里在实际当中的意思是把数据广播到任何一个节点
imBroadcast = sc.broadcast(itemMatrix)


现在可以计算每一个用户的推荐。这会对每一个用户因子进行一次map操作。在这个操作里,会对用户因子矩阵和电影因子矩阵做乘积,其结果为一个表示各个电影预计评级的向量(长度为1682,即电影的总数目)。之后,用预计评级对它们排序:

userVector = model.userFeatures().map(lambda (userId,array):(userId,np.array(array)))
#print userVector.collect()[0][1].shape
userVector = userVector.map(lambda (userId,x): (userId,imBroadcast.value.dot((np.array(x).transpose()))))
#print userVector.collect()[0][1].shape
userVectorId = userVector.map(lambda (userId,x) : (userId,[(xx,i) for i,xx in enumerate(x.tolist())]))
#print userVectorId.collect()[0]
sortUserVectorId = userVectorId.map(lambda (userId,x):(userId,sorted(x,key=lambda x:x[0],reverse=True)))
sortUserVectorRecId = sortUserVectorId.map(lambda (userId,x): (userId,[xx[1] for xx in x]))
#print sortUserVectorRecId.take(2)
sortUserVectorRecId.count()

输出结果:

943


userMovies = ratings.map(lambda rating: (rating.user,rating.product)).groupBy(lambda (x,y):x)
print userMovies.take(3)
userMovies = userMovies.map(lambda (userId,x):(userId, [xx[1] for xx in x] ))
#print userMovies.take(1)
#allAPK=sortUserVectorRecId.join(userMovies).map(lambda (userId,(predicted, actual)):(userId,avgPrecisionK(actual,predicted,10)))
#print allAPK.map(lambda (x,y):y).reduce(lambda x,y:x+y)/sortUserVectorRecId.count()
allAPK=sortUserVectorRecId.join(userMovies).map(lambda (userId,(predicted, actual)):avgPrecisionK(actual,predicted,2000))
# print allAPK.take(10)
print allAPK.reduce(lambda x,y:x+y)/allAPK.count()

输出结果:

[(2, <pyspark.resultiterable.ResultIterable object at 0x1163fa750>), 
(4, <pyspark.resultiterable.ResultIterable object at 0x1163fae90>), 
(6, <pyspark.resultiterable.ResultIterable object at 0x1163fa810>)]
0.0

我们模型的MAP得分相当低。但请注意,推荐类任务的这个得分通常都较低,特别是当物品的数量极大时。试着给lambdarank设置其他的值,看一下你能否找到一个RMSEMAPK得分更好的模型。





5.3 使用MLlib内置的评估函数


RMSEMSE

前面我们从零开始对模型进行了MSERMSEMAP三方面的评估。这是一段很有用的练习。同样, MLlib下的RegressionMetricsRankingMetrics类也提供了相应的函数以方便模型评估。

from pyspark.mllib.evaluation import RegressionMetrics
from pyspark.mllib.evaluation import RankingMetrics
predictedAndTrue = ratingsAndPredictions.map(lambda ((userId,product),(predicted, actual))
      :(predicted,actual))
print predictedAndTrue.take(5)
regressionMetrics = RegressionMetrics(predictedAndTrue)
print "均方误差 = %f"%regressionMetrics.meanSquaredError
print "均方根误差 = %f"% regressionMetrics.rootMeanSquaredError

输出结果:

[(5.0, 5.068769168364991), (3.0, 3.078863587357608), (1.0, 1.5594002961174487), (3.0, 2.822048986057712), (3.0, 3.190997207172113)]
均方误差 = 0.085127
均方根误差 = 0.291766


MAP

与计算MSERMSE一样,可以使用MLlibRankingMetrics类来计算基于排名的评估指标。类似地,需要向我们之前的平均准确率函数传入一个键值对类型的RDD。其键为给定用户预测的推荐物品的ID数组,而值则是实际的物品ID数组。

RankingMetrics中的K值平均准确率函数的实现与我们的有所不同,因而结果会不同。但全局平均准确率(Mean Average Precision,MAP,并不设定阈值K)会和当K值较大(比如设为总的物品数目)时我们模型的计算结果相同。

sortedLabels = sortUserVectorRecId.join(userMovies).map(lambda (userId,(predicted, actual)):(predicted,actual))
# print sortedLabels.take(1)
rankMetrics = RankingMetrics(sortedLabels)
print "Mean Average Precision = %f" % rankMetrics.meanAveragePrecision
print "Mean Average Precision(at K=10) = %f" % rankMetrics.precisionAt(10)

输出结果:

Mean Average Precision = 0.072366
Mean Average Precision(at K=10) = 0.069883


6 小结


博文,我们用SparkMLlib库训练了一个协同过滤推荐模型。我们也学会了如何使用该模型来向用户推荐他们可能会喜好的物品,以及找出和指定物品类似的物品。最后,我们用一些常见的指标来对该模型的预测能力进行了评估。

下一博文介绍如何使用Spark来训练一个分类模型,以及用标准的评估机制来衡量模型性能。






©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页