今天我們講一個下怎麼使用隨機遊走算法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),
- 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)
數據集使用的本文一開始講的那個,最終各個節點的概率結果如下所示:
上面的代碼是對本文一開始描述的數據集中的用戶A進行推薦。上圖給出了不同迭代次數後各節點的概率值。發現46次迭代之後,所有節點的概率值全都收斂。在這個例子中,A用戶沒有產生過行爲的物品是b和d,相對於A的訪問概率分別是0.039,0.076,d的訪問概率顯然要大於b,所有給A用戶的推薦列表爲{d,b}。
From:http://zhangxiong0301.iteye.com/blog/2249310