转自 http://blog.csdn.net/sinat_33741547/article/details/53002524
一 基本概念
基于图的模型是推荐系统中相当重要的一种方法,以下内容的基本思想是将用户行为数据表示为一系列的二元组,每一个二元组(u,i)代表用户u对物品i产生过行为,这样便可以将这个数据集表示为一个二分图。
假设我们有以下的数据集,只考虑用户喜不喜欢该物品而不考虑用户对物品的喜欢程度,
其中用户user=[A,B,C],物品item=[a,b,c],用户和物品有以下的关系:
上述便是一个典型的二分图,我们用G(V,E)来表示,其中V为用户user和物品item组成的顶点集即[A,B,C,a,b,c],而E则代表每一个二元组(u,i)之间对应的边e(u,i),我们这里不考虑用户对物品的喜爱程度,即默认喜爱则e=1,不喜爱则e=0。
那么我们如何使用上述的二分图模型进行物品的推荐呢?根据用户与物品的相关性,对于相关性高的顶点有如下的定义:
(1)两个顶点之间有很多路径相连
(2)连接两个顶点之间的路径长度都比较短
(3)连接两个顶点之间的路径不会经过度比较大的顶点
上面有一个概念需要理解,度,顶点的度是指和该顶点相关联的边数。
基于上述的定义,我们这里使用基于随机游走的PersonalRank算法来计算,那么这个算法是什么意思呢?
在解释之前,我们先理解一下另一个算法,pageRank算法,这个一个用来衡量搜索引擎中特定网页相对于其他网页重要性的算法,使用这个算法作为搜索结果网页排名相当重要的一部分。
它的基本思想是,假设网页之前通过超链接相互连接,互联网上的所有网页便构成了一张图。用户随机的打开一个网页,并通过超链接跳转到另一个网页。每当用户到达一个网页,他都有两种选择,停留在当前网页或者通过继续访问其他网页。如果用户继续访问网页的概率为alpha,那么用户停留在当前网页的概率便是1-alpha。如果用户继续访问其他网页,则会以均匀分布的方式随机访问当前网页指向的另一网页,这是一个随机游走的过程。当用户多次访问网页后,每一个网页被访问到的概率便会收敛到某个值,而计算出来的结果便可以用于网页排名,我们用以下的公式来表示:
其中PR(i)是网页i被访问到的概率,alpha代表用户继续访问网页的概率,N为所有网页的数量,in(i)代表所有指向网页i的网页集合,out(j)代表网页j指向的其他网页集合。
接下来我们分析一下这个公式,网页i被访问到的概率由两部分组成:
第一部分是网页i作为起点,第一个被和用户点击后停留在当前页面的概率,即:
第二部分是用户点击其他网页后(无论网页i是不是起点),再次跳转回到网页i的概率:
这两部分的和便是网页i被点击到的概率。
介绍完pageRank算法后,我们再来看看PersonalRank算法,这个算法是基于pageRank算法进行了一些变化,在pageRank算法中,计算出来的是每一个顶点相对其他顶点的相关性,代入到我们的用户物品二分图中,这显然不是我们想要的,我们需要的是所有物品相对于特定某个用户的相关性,有公式如下:
对比pageRank,不同点只在于r的值不同,root代表根节点,即我们的目标用户节点,意思便是我们每次都是从目标用户节点出发,进行随机游走,而不同于pageRank的起点是随机从所有网页中进行选择,personalRank算法得出的结果便是所有顶点相对于目标用户结点的相关性。
二 实战
数据集:采用movielens的1M数据集
1 获取ratings数据集的数据
1 def getResource(csvpath): 2 ''''' 3 获取原始数据 4 :param csvpath: csv路径 5 :return: frame 6 ''' 7 frame = pd.read_csv(csvpath) 8 return frame
2 整理用户及物品二分图,不考虑权重,只要用户对电影评过分便认为喜欢,e=1
(1)用户顶点二元组
def getUserGraph(frame, userID=1): ''''' 获取目标用户二分图, 不计权重 :param frame: ratings数据 :param userID: 目标ID :return: 二分图字典 ''' print(userID) itemList = list(set(frame[frame['UserID']==userID]['MovieID'])) graphDict = {'i'+str(item): 1 for item in itemList} return graphDict
(2)物品顶点二元组
def getItemGraph(frame, itemID=1): ''''' 获取目标物品二分图, 不计权重 :param frame: ratings数据 :param userID: 目标ID :return: 二分图字典 ''' print(itemID) userList = list(set(frame[frame['MovieID']==itemID]['UserID'])) graphDict = {'u'+str(user): 1 for user in userList} return graphDict
(3)整理成规范二分图G
def initGraph(frame): ''''' 初始化二分图 :param frame: ratings数据集 :return: 二分图 ''' userList = list(set(frame['UserID'])) itemList = list(set(frame['MovieID'])) G = {'u'+str(user): getUserGraph(frame, user) for user in userList} for item in itemList: G['i'+str(item)] = getItemGraph(frame, item) return G
3 利用PersonalRank算法进行计算
def personalRank(G, alpha, userID, iterCount=20): ''''' 随机游走迭代 :param G: 二分图 :param alpha: 随机游走的概率 :param userID: 目标用户 :param iterCount: 迭代次数 :return: series ''' rank = {g: 0 for g in G.keys()} rank['u'+str(userID)] = 1 #根节点为起点选择概率为1,其他顶点为0 for k in range(iterCount): tmp = {g: 0 for g in G.keys()} for i, ri in G.items(): #遍历每一个顶点 for j, wij in ri.items(): #遍历每个顶点连接的顶点 tmp[j] += alpha * rank[i] / len(ri) tmp['u' + str(userID)] += 1 - alpha #根顶点r=1,加上1-alpha rank = tmp series = pd.Series(list(rank.values()), index=list(rank.keys())) series = series.sort_values(ascending=False) return series #返回排序后的series
4 推荐和目标用户没有直接相连而且是物品的顶点
def recommend(frame, series, userID, TopN=10): ''''' 推荐TopN个用户没有评分的物品 :param frame: ratings数据 :param series: series :param userID: 目标用户 :param TopN: TopN :return: 推荐物品 ''' itemList = ['i'+str(i) for i in list(set(frame[frame['UserID']==userID]['MovieID']))] recommendList = [{u: series[u]} for u in list(series.index) if u not in itemList and 'u' not in u] return recommendList[:TopN]
5 迭代结果如下,可以看出经过15次左右的迭代,结果就基本趋于稳定了
[{'i3571': 0.0}, {'i830': 0.0}, {'i2942': 0.0}, {'i1725': 0.0}, {'i2445': 0.0}, {'i3822': 0.0}, {'i102': 0.0}, {'i2180': 0.0}, {'i264': 0.0}, {'i2615': 0.0}] [{'i3867': 0.0}, {'i3922': 0.0}, {'i2061': 0.0}, {'i1020': 0.0}, {'i1395': 0.0}, {'i1877': 0.0}, {'i135': 0.0}, {'i2341': 0.0}, {'i1115': 0.0}, {'i3308': 0.0}] [{'i2858': 0.00083046633303730691}, {'i1196': 0.00071671776671615873}, {'i1210': 0.00067806429079155233}, {'i593': 0.0006350175259251657}, {'i2396': 0.00061076227633024617}, {'i1198': 0.00060722720491067177}, {'i480': 0.00059923101084963497}, {'i2571': 0.00058276347994485863}, {'i318': 0.00058003203495347985}, {'i589': 0.00057600264742804167}] [{'i2858': 0.0003321865332149249}, {'i1196': 0.00028668710668646325}, {'i1210': 0.00027122571631662092}, {'i593': 0.00025400701037006602}, {'i2396': 0.00024430491053209827}, {'i1198': 0.00024289088196426857}, {'i480': 0.00023969240433985376}, {'i2571': 0.00023310539197794378}, {'i318': 0.00023201281398139187}, {'i589': 0.00023040105897121686}] [{'i2858': 0.00060349653055173386}, {'i1196': 0.00052196086934052385}, {'i1210': 0.00049776616762635677}, {'i593': 0.00045784751864878575}, {'i480': 0.00044765392929357667}, {'i1198': 0.0004410926232734298}, {'i589': 0.00043557149444675361}, {'i2571': 0.00043437223855112258}, {'i2396': 0.00043412826788239101}, {'i110': 0.00041807554733736419}] [{'i2858': 0.00044071053214964735}, {'i1196': 0.00038079661174808798}, {'i1210': 0.00036184189684051532}, {'i593': 0.00033554321368155346}, {'i480': 0.00032287701432134349}, {'i1198': 0.00032217157848793285}, {'i2396': 0.00032023425347221535}, {'i2571': 0.00031361213060721635}, {'i589': 0.00031246923316143255}, {'i110': 0.00030344760820981306}] [{'i2858': 0.00053694131852169088}, {'i1196': 0.00046458802316738787}, {'i1210': 0.00044259993946193073}, {'i593': 0.00040788262090828896}, {'i480': 0.00039758669454385479}, {'i1198': 0.00039265396543004474}, {'i2396': 0.00038687577725018962}, {'i589': 0.00038649101791954219}, {'i2571': 0.00038600611873183192}, {'i110': 0.00037182824192947688}] [{'i2858': 0.0004792028466984644}, {'i1196': 0.0004143131763158079}, {'i1210': 0.00039414511388908164}, {'i593': 0.00036447897657224798}, {'i480': 0.00035276088641034826}, {'i1198': 0.00035036453326477753}, {'i2396': 0.00034689086298340466}, {'i2571': 0.0003425697258570615}, {'i589': 0.00034207794706467542}, {'i110': 0.00033079986169767844}] [{'i2858': 0.00051376165005418437}, {'i1196': 0.00044444351663661672}, {'i1210': 0.00042319333008984872}, {'i593': 0.0003904669163388696}, {'i480': 0.00037967291853824856}, {'i1198': 0.00037570074620764637}, {'i2396': 0.00037078219926530098}, {'i589': 0.00036875659476582576}, {'i2571': 0.00036865439148347004}, {'i110': 0.00035541212342766543}] [{'i2858': 0.00049302636804075199}, {'i1196': 0.00042636531244413177}, {'i1210': 0.00040576440036938898}, {'i593': 0.00037487415247889689}, {'i480': 0.00036352569926150825}, {'i1198': 0.00036049901844192547}, {'i2396': 0.00035644739749616432}, {'i2571': 0.00035300359210762509}, {'i589': 0.00035274940614513588}, {'i110': 0.00034064476638967305}] [{'i2858': 0.00050546190424658681}, {'i1196': 0.00043721087645157386}, {'i1210': 0.0004162214547918957}, {'i593': 0.00038422614151956412}, {'i480': 0.00037321662425625601}, {'i1198': 0.0003696182084851502}, {'i2396': 0.00036504184399821651}, {'i2571': 0.00036239701620892931}, {'i589': 0.00036235686867035807}, {'i110': 0.0003495057342957748}] [{'i2858': 0.00049800058252308604}, {'i1196': 0.00043070353804710909}, {'i1210': 0.00040994722213839196}, {'i593': 0.00037861494809516286}, {'i480': 0.0003674020692594065}, {'i1198': 0.00036414669445921434}, {'i2396': 0.00035988517609698454}, {'i2571': 0.00035676096174814604}, {'i589': 0.00035659239115522413}, {'i110': 0.00034418915355211341}] [{'i2858': 0.00050247696507266541}, {'i1196': 0.00043460788156115355}, {'i1210': 0.00041371180788641855}, {'i593': 0.00038198137743991227}, {'i480': 0.0003708910680577479}, {'i1198': 0.00036742949422326811}, {'i2396': 0.00036297872662805993}, {'i2571': 0.00036014288057225089}, {'i589': 0.0003600513587190007}, {'i110': 0.00034737918324631687}] [{'i2858': 0.00049979113554291703}, {'i1196': 0.00043226527545272642}, {'i1210': 0.00041145305643760225}, {'i593': 0.00037996151983306353}, {'i480': 0.00036879766877874302}, {'i1198': 0.00036545981436483658}, {'i2396': 0.00036112259630941473}, {'i2571': 0.00035811372927778781}, {'i589': 0.0003579759781807344}, {'i110': 0.00034546516542979523}] [{'i2858': 0.00050140260178169259}, {'i1196': 0.0004336708357653612}, {'i1210': 0.00041280831402142163}, {'i593': 0.00038117341069881573}, {'i480': 0.00037005373269728003}, {'i1198': 0.00036664161465205691}, {'i2396': 0.00036223624105388892}, {'i2571': 0.00035933124573009519}, {'i589': 0.00035922123055598534}, {'i110': 0.00034661358443908758}] [{'i2858': 0.00050043572203842799}, {'i1196': 0.00043282749957778078}, {'i1210': 0.00041199515947113016}, {'i593': 0.00038044627617936426}, {'i480': 0.0003693000943461583}, {'i1198': 0.0003659325344797252}, {'i2396': 0.00036156805420720528}, {'i2571': 0.0003586007358587113}, {'i589': 0.00035847407913083547}, {'i110': 0.00034592453303351333}] [{'i2858': 0.00050101584739736013}, {'i1196': 0.0004333335010326456}, {'i1210': 0.00041248305287340628}, {'i593': 0.00038088255489434063}, {'i480': 0.00036975227950034447}, {'i1198': 0.00036635798197326311}, {'i2396': 0.00036196896372303849}, {'i2571': 0.00035903904402319342}, {'i589': 0.00035892237202062873}, {'i110': 0.00034633796465562979}] [{'i2858': 0.00050066777218199944}, {'i1196': 0.00043302990015972582}, {'i1210': 0.0004121903168320396}, {'i593': 0.00038062078766535462}, {'i480': 0.00036948096840783211}, {'i1198': 0.00036610271347714009}, {'i2396': 0.00036172841801353889}, {'i2571': 0.00035877605912450363}, {'i589': 0.00035865339628675218}, {'i110': 0.00034608990568235995}] [{'i2858': 0.00050087661711107777}, {'i1196': 0.00043321206065948117}, {'i1210': 0.00041236595851715116}, {'i593': 0.00038077784783332579}, {'i480': 0.00036964375524926104}, {'i1198': 0.00036625587452236827}, {'i2396': 0.00036187274523237836}, {'i2571': 0.00035893385025763939}, {'i589': 0.00035881478189912932}, {'i110': 0.00034623874113694431}] [{'i2858': 0.00050075131015363142}, {'i1196': 0.00043310276435962864}, {'i1210': 0.00041226057350608519}, {'i593': 0.00038068361173254341}, {'i480': 0.00036954608314440407}, {'i1198': 0.00036616397789523172}, {'i2396': 0.00036178614890107399}, {'i2571': 0.00035883917557775791}, {'i589': 0.00035871795053170325}, {'i110': 0.0003461494398641943}]
基于随机游走的personalrank算法实现推荐
今天我们讲一个下怎么使用随机游走算法PersonalRank实现基于图的推荐。
在推荐系统中,用户行为数据可以表示成图的形式,具体来说是二部图。用户的行为数据集由一个个(u,i)二元组组成,表示为用户u对物品i产生过行为。本文中我们认为用户对他产生过行为的物品的兴趣度是一样的,也就是我们只考虑“感兴趣”OR“不感兴趣”。假设有下图所示的行为数据集。
其中users集U={A, B, C},items集I = {a,b,c,d}。则用户物品的二部图如下所示:
我们用G(V, E)来表示这个图,则顶点集V=U∪I,图中的边则是由数据集中的二元组确定。二元组(u, i)表示u对i有过行为,则在图中表现为有边相连,即e(u,i)。【注意】,本文中我们不考虑各边的权重(即u对i的兴趣度),权重都默认为1。感兴趣即有边相连,不感兴趣则没有边相连。
那有了二部图之后我们要对u进行推荐物品,就转化为计算用户顶点u和与所有物品顶点之间的相关性,然后取与用户没有直接边相连的物品,按照相关性的高低生成推荐列表。说白了,这是一个图上的排名问题,我们最容易想到的就是Google的pageRank算法。
PageRank是Larry Page 和 Sergey Brin设计的用来衡量特定网页相对于搜索引擎中其他网页的重要性的算法,其计算结果作为google搜索结果中网页排名的重要指标。网页之间通过超链接相互连接,互联网上不计其数的网页就构成了一张超大的图。PageRank假设用户从所有网页中随机选择一个网页进行浏览,然后通过超链接在网页直接不断跳转。到达每个网页后,用户有两种选择:到此结束或者继续选择一个链接浏览。算法令用户继续浏览的概率为d,用户以相等的概率在当前页面的所有超链接中随机选择一个继续浏览。这是一个随机游走的过程。当经过很多次这样的游走之后,每个网页被访问用户访问到的概率就会收敛到一个稳定值。这个概率就是网页的重要性指标,被用于网页排名。算法迭代关系式如下所示:
上式中PR(i)是网页i的访问概率(也就是重要度),d是用户继续访问网页的概率,N是网页总数。in(i)表示指向网页i的网页集合,out(j)表示网页j指向的网页集合。
用user节点和item节点替换上面的网页节点就可以计算出每个user,每个item在全局的重要性,给出全局的排名,显然这并不是我们想要的,我们需要计算的是物品节点相对于某一个用户节点u的相关性。怎么做呢?Standford的Haveliwala于2002年在他《Topic-sensitive pagerank》一文中提出了PersonalRank算法,该算法能够为用户个性化的对所有物品进行排序。它的迭代公式如下:
我们发现PersonalRank跟PageRank的区别只是用替换了1/N,也就是说从不同点开始的概率不同。u表示我们推荐的目标用户,这样使用上式计算的就是所有顶点相对于顶点u的相关度。
与PageRank随机选择一个点开始游走(也就是说从每个点开始的概率都是相同的)不同,如果我们要计算所有节点相对于用户u的相关度,则PersonalRank从用户u对应的节点开始游走,每到一个节点都以1-d的概率停止游走并从u重新开始,或者以d的概率继续游走,从当前节点指向的节点中按照均匀分布随机选择一个节点往下游走。这样经过很多轮游走之后,每个顶点被访问到的概率也会收敛趋于稳定,这个时候我们就可以用概率来进行排名了。
在执行算法之前,我们需要初始化每个节点的初始概率值。如果我们对用户u进行推荐,则令u对应的节点的初始访问概率为1,其他节点的初始访问概率为0,然后再使用迭代公式计算。而对于pageRank来说,由于每个节点的初始访问概率相同,所以所有节点的初始访问概率都是1/N (N是节点总数)。
我自己用Python实现了一下PersonalRank:(可执行,感兴趣的童鞋可通过附件下载源码文件,若有错误恳请指正^_^)
#coding=utf-8 __author__ = 'Harry Huang' def PersonalRank(G, alpha, root, max_step): rank = dict() rank = {x:0 for x in G.keys()} rank[root] = 1 #开始迭代 for k in range(max_step): tmp = {x:0 for x in G.keys()} #取节点i和它的出边尾节点集合ri for i, ri in G.items(): #取节点i的出边的尾节点j以及边E(i,j)的权重wij, 边的权重都为1,在这不起实际作用 for j, wij in ri.items(): #i是j的其中一条入边的首节点,因此需要遍历图找到j的入边的首节点, #这个遍历过程就是此处的2层for循环,一次遍历就是一次游走 tmp[j] += alpha * rank[i] / (1.0 * len(ri)) #我们每次游走都是从root节点出发,因此root节点的权重需要加上(1 - alpha) #在《推荐系统实践》上,作者把这一句放在for j, wij in ri.items()这个循环下,我认为是有问题。 tmp[root] += (1 - alpha) rank = tmp #输出每次迭代后各个节点的权重 print 'iter: ' + str(k) + " ", for key, value in rank.items(): print "%s:%.3f, "%(key, value), print return rank if __name__ == '__main__' : G = {'A' : {'a' : 1, 'c' : 1}, 'B' : {'a' : 1, 'b' : 1, 'c':1, 'd':1}, 'C' : {'c' : 1, 'd' : 1}, 'a' : {'A' : 1, 'B' : 1}, 'b' : {'B' : 1}, 'c' : {'A' : 1, 'B' : 1, 'C':1}, 'd' : {'B' : 1, 'C' : 1}} PersonalRank(G, 0.85, 'A', 100)