作者:vivo互联网服务器团队-TangShutao
现如今推荐无处不在,例如抖音、淘宝、京东App均能见到推荐系统的身影,其背后涉及许多的技术。
本文以经典的协同过滤为切入点,重点介绍了被工业界广泛使用的矩阵分解算法,从理论与实践两个维度介绍了该算法的原理,通俗易懂,希望能够给大家带来一些启发。
笔者认为要彻底搞懂一篇论文,最好的方式就是动手复现它,复现的过程你会遇到各种各样的疑惑、理论细节。
一、背景
1.1引言
在信息爆炸的二十一世纪,人们很容易淹没在知识的海洋中,在该场景下搜索引擎可以帮助我们迅速找到我们想要查找的内容。
在电商场景,如今的社会物质极大丰富,商品琳琅满目,种类繁多。消费者很容易挑花眼,即用户将会面临信息过载的问题。
为了解决该问题,推荐引擎应运而生。例如我们打开淘宝App,JDapp,B站视频app,每一个场景下都有推荐的模块。
那么此时有一个幼儿园小朋友突然问你,为什么JD给你推荐这本《程序员颈椎康复指南》?你可能会回答,因为我的职业是程序员。
接着小朋友又问,为什么《Spark大数据分析》这本书排在第6个推荐位,而《Scala编程》排在第2位?这时你可能无法回答这个问题。
为了回答该问题,我们设想下面的场景:
在JD的电商系统中,存在着用户和商品两种角色,并且我们假设用户都会对自己购买的商品打一个0-5之间的分数,分数越高代表越喜欢该商品。
基于此假设,我们将上面的问题转化为用户对《程序员颈椎康复指南》,《Spark大数据分析》,《Scala编程》这三本书打分的话,用户会打多少分(用户之前未购买过这3本书)。因此物品在页面的先后顺序就等价于预测用户对这些物品的评分,并且根据这些评分进行排序的问题。
为了便于预测用户对物品的评分问题,我们将所有三元组(User,Item,Rating),即用户User给自己购买的商品Item的评分为Rating,组织为如下的矩阵形式:
其中,表格包含m个用户和n个物品,将表格定义为评分矩阵Rm×n"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Rm×nRm×n,其中的元素ru,i"role="presentation">ru,iru,i表示第u个用户对第i个物品的评分。
例如,在上面的表格中,用户user-1购买了物品item-1,item-3,item-4,并且分别给出了4,2,5的评分。最终,我们将原问题转化为预测白色空格处的数值。
1.2协同过滤
协同过滤,简单来说是利用与用户兴趣相投、拥有共同经验之群体的喜好来推荐给用户感兴趣的物品。兴趣相投使用数学语言来表达就是相似度(人与人,物与物)。因此,根据相似度的对象,协同过滤可以分为基于用户的协同过滤和基于物品的协同过滤。
以评分矩阵为例,以行方向观测评分矩阵,每一行代表每个用户的向量表示,例如用户user-1的向量为[4,0,2,5,0,0]。以列方向观测评分矩阵,每一列表示每个物品的向量表示,例如物品item-1的向量为[4,3,0,0,5]。
基于向量表示,相似度的计算有多种公式,例如余弦相似度,欧氏距离,皮尔森。这里我们以余弦相似度为例,它是我们中学学过的向量夹角(中学只涉及2维和3维)的高维推广,余弦相似度公式很容易理解和使用。给定两个向量A={a1,,an}"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">A={a1,,an}A={a1,,an}和B={b1,,bn}"role="presentation">B={b1,,bn}B={b1,,bn},其夹角定义如下:
例如,我们计算user-3和user-4的余弦相似度,二者对应的向量分别为[0,2,0,3,0,4],[0,3,3,5,4,0]
向量夹角的余弦值越接近1代表两个物品方向越接近平行,也就是越相似,反之越接近-1代表两个物品方向越接近反向,表示两个物品相似度接近相反,接近0,表示向量接近垂直/正交,两个物品几乎无关联。显然,这和人的直觉完全一致。
我们用《血族第一部》在向量库(存储向量的数据库,该系统能够根据输入向量,用相似度公式在库中进行检索,找出TopN的候选向量)里面进行相似度检索,找到了前7部高相似度的影片,值得注意的是第一部是自己本身,相似度为1.0,其他三部是《血族》的其他3部同系列作品。
1.2.1基于用户的协同过滤(UserCF)
基于用户的协同过滤分为两步
找出用户相似度TopN的若干用户。
根据TopN用户评分的物品,形成候选物品集合,利用加权平均预估用户u对每个候选物品的评分。
例如,由用户u的相似用户{u1,u3,u5,u9}可得候选物品为
我们现在预测用户u对物品i1的评分,由于物品在两个用户{u1,u5}的购买记录里,因此用户u对物品i1的预测评分为:
其中sim(u,u1)"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">sim(u,u1)sim(u,u1)表示用户u与用户u1"role="presentation">u1u1的相似度。
在推荐时,根据用户u对所有候选物品的预测分进行排序,取TopM的候选物品推荐给用户u即可。
1.2.2基于物品的协同过滤(ItemCF)
基于物品的协同过滤分为两步
在用户u购买的物品集合中,选取与每一个物品TopN相似的物品。
TopN相似物品形成候选物品集合,利用加权平均预估用户u对每个候选物品的评分。
例如,我们预测用户u对物品i3的评分,由于物品i3与物品{i6,i1,i9}均相似,因此用户u对物品i3的预测评分为:
1.2.3UserCF与ItemCF的比较
我们对ItemCF和UserCF做如下总结:
ItemCF给用户推荐那些和他之前喜欢的物品类似的物品,即ItemCF的推荐结果着重于维系用户的历史兴趣,推荐更加个性化,反应用户自己的兴趣。在实际应用中,图书、电影平台使用ItemCF,比如豆瓣、亚马逊、Netflix等。
除了基于用户和基于物品的协同过滤,还有一类基于模型的协同过滤算法,如上图所示。此外基于用户和基于物品的协同过滤又可以归类为基于邻域(K-NearestNeighbor,KNN)的算法,本质都是在找"TopN邻居",然后利用邻居和相似度进行预测。
二、矩阵分解
经典的协同过滤算法本身存在一些缺点,其中最明显的就是稀疏性问题。我们知道评分矩阵是一个大型稀疏矩阵,导致在计算相似度时,两个向量的点积等于0(以余弦相似度为例)。为了更直观的理解这一点,我们举例如下:
romsklearn.metrics.pairwiseimportcosine_similaritya=[[0,0,0,3,2,0,3.5,0,1],[0,1,0,0,0,0,0,0,0],[0,0,1,0,0,0,0,0,0],[4.1,3.8,4.6,3.8,4.4,3,4,0,3.6]cosine_similarity(a)#array([[1.,0.,0.,0.66209271],#[0.,1.,0.,0.34101639],#[0.,0.,1.,0.41280932],#[0.66209271,0.34101639,0.41280932,1.]])
我们从评分矩阵中抽取item1-item4的向量,并且利用余弦相似度计算它们之间的相似度。
通过相似度矩阵,我们可以看到物品item-1,item-2,item-3的之间的相似度均为0,而且与item-1,item-2,item-3最相似的物品都是item-4,因此在以ItemCF为基础的推荐场景中item-4将会被推荐给用户。
综上,我们可以看到经典的基于用户/物品的协同过滤算法有天然的缺陷,无法处理稀疏场景。为了解决该问题,矩阵分解被提出。
2.1显示反馈
我们将用户对物品的评分行为定义为显示反馈。基于显示反馈的矩阵分解是将评分矩阵Rm×n用两个矩阵Xm×k"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Xm×k和Yn×k的乘积近似表示,其数学表示如下:
为了求解矩阵Xm×k"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Xm×kXm×k和Yn×k,需要最小化平方误差损失函数,来尽可能地使得两个矩阵的乘积逼近评分矩阵Rm×n"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Rm×nRm×n,即
其中,λ(∑uxuTxu+∑iyiTyi)"role="presentation">λ(∑uxTuxu+∑iyTiyi)λ(∑uxuTxu+∑iyiTyi)为惩罚项,λ为惩罚系数/正则化系数,xu表示第u个用户的k维特征向量,yi"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">yiyi表示第i个物品的k维特征向量。
全体用户的特征向量构成了用户矩阵Xm×k"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Xm×kXm×k,全体物品的特征向量构成了物品矩阵Yn×k。
我们训练模型的时候,就只需要训练用户矩阵中的m×k个参数和物品矩阵中的n×k个参数。因此,协同过滤就成功转化成了一个优化问题。
2.2预测评分
通过模型训练(即求解模型系数的过程),我们得到用户矩阵Xm×k和物品矩阵Yn×k"role="presentation">Yn×kYn×k,全部用户对全部物品的评分预测可以通过Xm×k(Yn×k)T"role="presentation">Xm×k(Yn×k)TXm×k(Yn×k)T获得。如下图所示。
得到全部的评分预测后,我们就可以对每个物品进行择优推荐。需要注意的是,用户矩阵和物品矩阵的乘积,得到的评分预估值,与用户的实际评分不是全等关系,而是近似相等的关系。如上图中两个矩阵粉色部分,用户实际评分和预估评分都是近似的,有一定的误差。
2.3理论推导
矩阵分解ALS的理论推导网上也有不少,但是很多推导不是那么严谨,在操作向量导数时有的步骤甚至是错误的。有的博主对损失函数的求和项理解解出现错误,例如
但是评分矩阵是稀疏的,求和并不会贯穿整个用户集和物品集。正确的写法应该是
其中,(u,i)isknown"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">(u,i)isknown(u,i)isknown表示已知的评分项。
我们在本节给出详细的、正确的推导过程,一是当做数学小练习,其次也是对算法有更深层的理解,便于阅读SparkALS的源码。
将(u,i)isknown"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">(u,i)isknown(u,i)isknown使用数学语言描述,矩阵分解的损失函数定义如下:
其中K为评分矩阵中已知的(u,i)集合。例如下面的评分矩阵对应的K为
求解上述损失函数存在两种典型的优化方法,分别为
交替最小二乘(AlternatingLeastSquares,ALS)
随机梯度下降(StochasticGradientDescent,SGD)
交替最小二乘,指的是固定其中一个变量,利用最小二乘求解另一个变量,以此交替进行,直至收敛或者到达最大迭代次数,这也是“交替”一词的由来。
随机梯度下降,是优化理论中最常用的一种方式,通过计算梯度,然后更新待求的变量。
在矩阵分解算法中,Spark最终选择了ALS作为官方的唯一实现,原因是ALS很容易实现并行化,任务之间没有依赖。
下面我们动手推导一下整个计算过程,在机器学习理论中,微分的单位一般在向量维度,很少去对向量的分量为偏微分推导。
首先我们固定物品矩阵Y,将物品矩阵Y看成常量。不失一般性,我们定义用户u评分过的物品集合为Iu"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">IuIu,利用损失函数对向量xu"role="presentation">xuxu求偏导,并且令导数等于0可得:
等式两边取转置,我们有
为了化简∑i∈IuyiyiT"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">∑i∈IuyiyTi∑i∈IuyiyiT与
∑i∈Iuru,iyi"role="presentation">∑i∈Iuru,iyi∑i∈Iuru,iyi,我们将Iu"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Iu"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Iu展开。
假设Iu={ic1,,icN},其中N表示用户u评分过的物品数量,ici"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">iciici表示第ci"role="presentation">cici个物品对应的索引/序号,借助于Iu"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;background-color:rgb(255,255,255);">Iu,我们有
其中,
YIu"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">YIuYIu为以Iu={ic1,icN}"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Iu={ic1,icN}Iu={ic1,icN}为行号在物品矩阵Y中选取的N个行向量形成的子矩阵
Ru,Iu为以Iu={ic1,icN}为索引,在评分矩阵R的第u行的行向量中选取的N个元素,形成的子行向量
因此,我们有
网上的博客,许多博主给出类似下面形式的结论不是很严谨,主要是损失函数的理解不到位导致的。
同理,我们定义物品i被评分的用户集合为Ui={ud1,udM}"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Ui={ud1,udM}Ui={ud1,udM}
根据对称性可得
XUi"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">XUiXUi为以Ui={ud1,,udM}"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Ui={ud1,,udM}Ui={ud1,,udM}为行号在用户矩阵X中选取的M个行向量形成的子矩阵
Ri,Ui"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Ri,UiRi,Ui为以Ui={ud1,,udM}"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">Ui={ud1,,udM}Ui={ud1,,udM}为索引,在评分矩阵R的第i列的列向量中选取的M个元素,形成的子列向量
此外,Ik"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">IkIk为单位矩阵
如果读者感觉上述的推导还是很抽象,我们也给一个具体实例来体会一下中间过程
注意到损失函数是一个标量,这里我们只展开涉及到x1,1,x1,2"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">x1,1,x1,2x1,1,x1,2的项,如下所示
让损失函数对x1,1,x1,2"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">x1,1,x1,2x1,1,x1,2分别求偏导数可以得到
写成矩阵形式可得
利用我们上述的规则,很容易检验我们导出的结论。
总结来说,ALS的整个算法过程只有两步,涉及2个循环,如下图所示:
算法使用RMSE(root-mean-squareerror)评估误差。
当RMSE值变化很小时或者到达最大迭代步骤时,满足收敛条件,停止迭代。
“Talkischeap.Showmethecode.”作为小练习,我们给出上述伪代码的Python实现。
#迭代10次foriterinrange(10):foruinrange(1,m+1):Iu=np.array(X_idx_dict[u])YIu=Y[Iu-1]YIuT=YIu.TRuIu=R[u-1,Iu-1]xu=linear_solve(YIuT.dot(YIu)+_lambda*np.eye(k),YIuT.dot(RuIu))X[u-1]=xuforiinrange(1,n+1):Ui=np.array(Y_idx_dict[i])XUi=X[Ui-1]XUiT=XUi.TRiUi=R.T[i-1,Ui-1]yi=linear_solve(XUiT.dot(XUi)+_lambda*np.eye(k),XUiT.dot(RiUi))Y[i-1]=yi
最终,我们打印用户矩阵,物品矩阵,预测的评分矩阵如下,可以看到预测的评分矩阵非常逼近原始评分矩阵。
#Xarray([[1.30678487,2.03300876,3.70447639],[4.96150381,1.03500693,1.62261161],[6.37691007,2.4290095,1.03465981],[0.41680155,3.31805612,3.24755801],[1.26803845,3.57580564,2.08450113]])#Yarray([[0.24891282,1.07434519,0.40258993],[0.12832662,0.17923216,0.72376732],[-0.00149517,0.77412863,0.12191856],[0.12398438,0.46163336,1.05188691],[0.07668894,0.61050204,0.59753081],[0.53437855,0.20862131,0.08185176]])#X.dot(Y.T)预测评分array([[4.00081359,3.2132548,2.02350084,4.9972158,3.55491072,1.42566466],[3.00018371,1.99659282,0.99163666,2.79974661,1.98192672,3.00005934],[4.61343295,2.00253692,1.99697545,3.00029418,2.59019481,3.99911584],[4.97591903,2.99866546,2.96391664,4.99946603,3.99816006,1.18076534],[4.99647978,2.31231627,3.02037696,4.0005876,3.5258348,1.59422188]])#原始评分矩阵array([[4,0,2,5,0,0],[3,2,1,0,0,3],[0,2,0,3,0,4],[0,3,3,5,4,0],[5,0,3,4,0,0]])
三、SparkALS应用
Spark的内部实现并不是我们上面所列的算法,但是核心原理是完全一样的,Spark实现的是上述伪代码的分布式版本,具体算法参考Large-scaleParallelCollaborativeFilteringfortheNetflixPrize。其次,查阅Spark的官方文档,我们也注意到,Spark使用的惩罚函数与我们上文的有细微的差别。
其中nu,ni"role="presentation"SegoeUI",Roboto,Oxygen,Ubuntu,"FiraSans","DroidSans","HelveticaNeue",sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;orphans:2;widows:2;-webkit-text-stroke-width:0px;background-color:rgb(255,255,255);text-decoration-thickness:initial;text-decoration-style:initial;text-decoration-color:initial;">nu,ninu,ni分别表示用户u打分的物品数量和物品i被打分的用户数量。即
本小节通过两个案例来了解SparkALS的具体使用,以及在面对互联网实际工程场景下的应用。
3.1Demo案例
以第一节给出的数据为例,将三元组(User,Item,Rating)组织为als-demo-data.csv,该demo数据集涉及5个用户和6个物品。
userId,itemId,rating1,1,41,3,21,4,52,1,32,2,22,3,12,6,33,2,23,4,33,6,44,2,34,3,34,4,54,5,45,1,55,3,35,4,4
使用Spark的ALS类使用非常简单,只需将三元组(User,Item,Rating)数据输入模型进行训练。
importorg.apache.spark.sql.SparkSessionimportorg.apache.spark.ml.recommendation.ALSvalspark=SparkSession.builder().appName("als-demo").master("local[*]").getOrCreate()valrating=spark.read.options(Map("inferSchema"->"true","delimiter"->",","header"->"true")).csv("./data/als-demo-data.csv")//展示前5条评分记录rating.show(5)valals=newALS().setMaxIter(10)//迭代次数,用于最小二乘交替迭代的次数.setRank(3)//隐向量的维度.setRegParam(0.01)//惩罚系数.setUserCol("userId")//user_id.setItemCol("itemId")//item_id.setRatingCol("rating")//评分列valmodel=als.fit(rating)//训练模型//打印用户向量和物品向量model.userFactors.show(truncate=false)model.itemFactors.show(truncate=false)//给所有用户推荐2个物品model.recommendForAllUsers(2).show()
上述代码在控制台输出结果如下:
|userId|itemId|rating||1|1|4||1|3|2||1|4|5||2|1|3||2|2|2|onlyshowingtop5rows|id|features||1|[-0.17339179,1.3144133,0.04453602]||2|[-0.3189066,1.0291641,0.12700711]||3|[-0.6425665,1.2283803,0.26179287]||4|[0.5160747,0.81320006,-0.57953185]||5|[0.645193,0.26639006,0.68648624]||id|features||1|[2.609607,3.2668495,3.554771]||2|[0.85432494,2.3137972,-1.1198239]||3|[3.280517,1.9563107,0.51483333]||4|[3.7446978,4.259611,0.6640027]||5|[1.6036265,2.5602736,-1.8897828]||6|[-1.2651576,2.4723763,0.51556784]||userId|recommendations||1|[[4,4.9791617],[1,3.9998217]]|//对应物品的序号和预测评分|2|[[4,3.273963],[6,3.0134287]]||3|[[6,3.9849386],[1,3.2667015]]||4|[[4,5.011649],[5,4.004795]]||5|[[1,4.994258],[4,4.0065994]]|
我们使用numpy来验证Spark的结果,并且用Excel可视化评分矩阵。
importnumpyasnpX=np.array([[-0.17339179,1.3144133,0.04453602],[-0.3189066,1.0291641,0.12700711],[-0.6425665,1.2283803,0.26179287],[0.5160747,0.81320006,-0.57953185],[0.645193,0.26639006,0.68648624]])Y=np.array([[2.609607,3.2668495,3.554771],[0.85432494,2.3137972,-1.1198239],[3.280517,1.9563107,0.51483333],[3.7446978,4.259611,0.6640027],[1.6036265,2.5602736,-1.8897828],[-1.2651576,2.4723...