轉自 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) + "\t", for key, value in rank.items(): print "%s:%.3f, \t"%(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)