《推薦系統實戰》中介紹了基於圖的推薦算法,將用戶行爲數據表示成圖的形式。Standford的Haveliwala於2002年在他《Topic-sensitive pagerank》一文中提出了PersonalRank算法。原理上與PageRank是很相似的,區別在於PageRank 中的鏈接是有向的,而PersonalRank中人於物品之間的連接是無向的,或者說是雙向的。
1.1 用戶行爲的二分圖表示
假設用戶行爲數據是由一系列二元數組組成的,每個二元組(u, i)表示用戶u對物品i產生過行爲。這種數據集可以用一個二分圖(Bipartite)表示,又叫二部圖。
令G(V, E)表示用戶物品二分圖,其中由用戶頂點集合和物品頂點集合組成。每個二元組(u, i)對應途中的一條邊,其中是用戶u對應的頂點,是物品i對應的頂點。
下圖是一個簡單的用戶物品二分圖模型,其中圓形節點代表用戶,方形節點代表物品,圓形節點和方形節點之間的邊代表用戶對物品的行爲。比如圖中用戶節點A和物品節點a、b、d相連,說明用戶A對物品a、b、d產生過行爲。
【圖2-18】
1.2 圖中頂點的相關性
將用戶行爲表示爲二分圖模型後,下面的任務就是在二分圖上給用戶進行個性化推薦。如果將個性化推薦算法放到二分圖模型上,那麼給用戶u推薦物品的任務就可以轉化爲度量用戶頂點和與沒有邊直接相連的物品節點在圖上的相關性,相關性越高的物品在推薦列表中的權重就越高。
度量圖中兩個頂點之間相關性的方法很多,但一般來說圖中頂點的相關性主要取決於下面3個因素:
- 兩個頂點之間的路徑數;(路徑越多相關性越高)
- 兩個頂點之間路徑的長度;(兩個頂點之間的路徑長度大都比較短,則相關性高)
- 兩個頂點之間的路徑經過的頂點。(兩頂點間的路徑不經過出度比較大的頂點,則相關性高)
舉一個簡單的例子,如圖2-19所示,用戶A和物品c、e沒有邊相連,但是用戶A和物品c有1條長度爲3的路徑相連,用戶A和物品e有2條長度爲3的路徑相連。那麼,頂點A與e之間的相關性要高於頂點A與c,因而物品e在用戶A的推薦列表中應該排在物品c之前,因爲頂點A與e之間有兩條路徑——(A, b, C, e)和(A, d, D, e)。其中,(A, b, C, e)路徑經過的頂點的出度爲(3, 2, 2, 2),而(A, d, D, e)路徑經過的頂點的出度爲(3, 2, 3, 2)。因此,(A, d, D, e)經過了一個出度比較大的頂點D,所以(A, d, D, e)對頂點A與e之間相關性的貢獻要小於(A, b, C, e)。
【圖2-19】
1.3 基於隨機遊走的PersonalRank算法
假設要給用戶u進行個性化推薦,可以從用戶u對應的節點開始在用戶物品二分圖上進行隨機遊走。遊走到任何一個節點時,首先按照概率 α 決定是繼續遊走,還是停止這次遊走並從節點開始重新遊走。如果決定繼續遊走,那麼就從當前節點指向的節點中按照均勻分佈隨機選擇一個節點作爲遊走下次經過的節點。這樣,經過很多次隨機遊走後,每個物品節點被訪問到的概率會收斂到一個數。最終的推薦列表中物品的權重就是物品節點的訪問概率。
上面的描述寫成公式就是:
其中,表示隨機遊走的概率;
PR(v’)表示上一次迭代頂點v’的重要度(在PageRank中PR(i)是網頁i的訪問概率,也就是重要度),賦初值迭代收斂得到;
在PageRank中out(j)表示網頁j指向的網頁集合,也就是j的出度。這裏out(v’)也表示節點v’指向的頂點集合。
1.4 缺點和改進
雖然PersonalRank算法可以通過隨機遊走進行比較好的理論解釋,但該算法在時間複雜度上有明顯的缺點。因爲在爲每個用戶進行推薦時,都需要在整個用戶物品二分圖上進行迭代,直到整個圖上的每個頂點的PR值收斂。這一過程的時間複雜度非常高,不僅無法在線提供實時推薦,甚至離線生成推薦結果也很耗時。
爲了解決PersonalRank每次都需要在全圖迭代並因此造成時間複雜度很高的問題,<<推薦系統實戰>>給出兩種解決方案。第一種很容易想到,就是減少迭代次數,在收斂之前就停止。這樣會影響最終的精度,但一般來說影響不會特別大。另一種方法就是從矩陣論出發,重新設計算法。
令M爲用戶物品二分圖的轉移概率矩陣:
那麼,迭代公式可以轉化爲:
用矩陣論的方法解出上面的方程,得到:
因此,只需要計算一次,這裏是稀疏矩陣。可以通過稀疏矩陣快速求逆來得解(比如Generalized_minimal_residual_method)。
在scipy中提供了多種稀疏矩陣的存儲方法:coo,lil,dia,dok,csr,csc等,各有各的優缺點,dok可以快速的按下標訪問元素,csr和csc適合做矩陣的加法、乘法運算,lil省內存且按下標訪問元素也很快。
參考實現:
https://github.com/lpty/recommendation
另外jamest給出了矩陣實現,代碼如下:
#-*-coding:utf-8-*-
"""
author:jamest
date:20190310
PersonalRank function with Matrix
"""
import pandas as pd
import numpy as np
import time
import operator
from scipy.sparse import coo_matrix
from scipy.sparse.linalg import gmres
class PersonalRank:
def __init__(self,X,Y):
X,Y = ['user_'+str(x) for x in X],['item_'+str(y) for y in Y]
self.G = self.get_graph(X,Y)
def get_graph(self,X,Y):
"""
Args:
X: user id
Y: item id
Returns:
graph:dic['user_id1':{'item_id1':1}, ... ]
"""
item_user = dict()
for i in range(len(X)):
user = X[i]
item = Y[i]
if item not in item_user:
item_user[item] = {}
item_user[item][user]=1
user_item = dict()
for i in range(len(Y)):
user = X[i]
item = Y[i]
if user not in user_item:
user_item[user] = {}
user_item[user][item]=1
G = dict(item_user,**user_item)
return G
def graph_to_m(self):
"""
Returns:
a coo_matrix sparse mat M
a list,total user item points
a dict,map all the point to row index
"""
graph = self.G
vertex = list(graph.keys())
address_dict = {}
total_len = len(vertex)
for index in range(len(vertex)):
address_dict[vertex[index]] = index
row = []
col = []
data = []
for element_i in graph:
weight = round(1/len(graph[element_i]),3)
row_index= address_dict[element_i]
for element_j in graph[element_i]:
col_index = address_dict[element_j]
row.append(row_index)
col.append(col_index)
data.append(weight)
row = np.array(row)
col = np.array(col)
data = np.array(data)
m = coo_matrix((data,(row,col)),shape=(total_len,total_len))
return m,vertex,address_dict
def mat_all_point(self,m_mat,vertex,alpha):
"""
get E-alpha*m_mat.T
Args:
m_mat
vertex:total item and user points
alpha:the prob for random walking
Returns:
a sparse
"""
total_len = len(vertex)
row = []
col = []
data = []
for index in range(total_len):
row.append(index)
col.append(index)
data.append(1)
row = np.array(row)
col = np.array(col)
data = np.array(data)
eye_t = coo_matrix((data,(row,col)),shape=(total_len,total_len))
return eye_t.tocsr()-alpha*m_mat.tocsr().transpose()
def recommend_use_matrix(self, alpha, userID, K=10,use_matrix=True):
"""
Args:
alpha:the prob for random walking
userID:the user to recom
K:recom item num
Returns:
a dic,key:itemid ,value:pr score
"""
m, vertex, address_dict = self.graph_to_m()
userID = 'user_' + str(userID)
print('add',address_dict)
if userID not in address_dict:
return []
score_dict = {}
recom_dict = {}
mat_all = self.mat_all_point(m,vertex,alpha)
index = address_dict[userID]
initial_list = [[0] for row in range(len(vertex))]
initial_list[index] = [1]
r_zero = np.array(initial_list)
res = gmres(mat_all,r_zero,tol=1e-8)[0]
for index in range(len(res)):
point = vertex[index]
if len(point.strip().split('_'))<2:
continue
if point in self.G[userID]:
continue
score_dict[point] = round(res[index],3)
for zuhe in sorted(score_dict.items(),key=operator.itemgetter(1),reverse=True)[:K]:
point,score = zuhe[0],zuhe[1]
recom_dict[point] = score
return recom_dict
if __name__ == '__main__':
moviesPath = '../data/ml-1m/movies.dat'
ratingsPath = '../data/ml-1m/ratings.dat'
usersPath = '../data/ml-1m/users.dat'
# usersDF = pd.read_csv(usersPath,index_col=None,sep='::',
header=None,names=['user_id', 'gender', 'age', 'occupation', 'zip'])
# moviesDF = pd.read_csv(moviesPath,index_col=None,sep='::',
header=None,names=['movie_id', 'title', 'genres'])
ratingsDF = pd.read_csv(ratingsPath, index_col=None, sep='::', header=None,
names=['user_id', 'movie_id', 'rating', 'timestamp'])
X=ratingsDF['user_id'][:1000]
Y=ratingsDF['movie_id'][:1000]
rank = PersonalRank(X,Y).recommend_use_matrix(alpha=0.8,userID=1,K=30)
print('PersonalRank result',rank)
Stanford有一個快速計算Personal PageRank的Talk
其開源代碼是scala寫的。
部分摘錄如下:
Personalized PageRank簡介:
Given: 源 s, 目的 t,以及"teleport probability"(佩奇命名的科幻瞬間轉移概率)
- 從源s開始隨機遊走;
- 每一步,以的概率停止前進,否則continue。
那麼給出從 s 到 t 的Personalized PageRank:
- 等價於特徵向量(eigenvector)/平穩分佈的定義。若馬爾可夫鏈在n+1時刻狀態空間的分佈與n時刻的分佈相同,則稱
此分佈爲平穩分佈。若馬爾可夫鏈的初始狀態服從平穩分佈,則該馬爾可夫鏈爲平穩過程。 - FAST-PPR (快速personal PageRank)允許任意的起點集合,例如:
Global PageRank
personalize to S
目標
給定 ,出發節點 s,單個的目的節點 t ,threshold
目標是估計:
當時:
- 源自於個性化搜索;
- 只要,而不是整個向量;
- 由於平均的 是 ,我們希望 。考慮實時性,運行時間必須。
根據
以前的蒙特卡洛算法以的複雜度從s開始隨機遊走,運行時間是
以前的本地更新算法從目的節點t 沿着邊反向遊走,並在本地更新Personal PageRank值,平均運行時間爲,其中(邊的個數/頂點個數)
得到定理:
Main idea: