基于图的模型( )是推荐系统中的重要内容。在研究基于图的模型之前,首先需要将用户行为数据表示成图的形式。这里我们将用户行为数据用二分图表示,例如用户数据是由一系列的二元组(也可以使用列表)组成,其中每个元组 表示用户 对物品 产生过行为。下图为 , , 用户感兴趣的音乐:
产生的二分图模型如下:
将用户行为表示为二分图模型后,下面的任务就是在二分图上给用户进行个性化推荐。如果将个性化推荐算法放到二分图模型上,那么给用户
推荐物品的任务就可以转化为度量用户顶点
和与
没有边直接相连的物品节点在图上的相关性,相关性越高的物品在推荐列表中的权重就越高。
度量图中两个顶点之间相关性的方法很多,但一般来说图中顶点的相关性主要取决于下面3个因素:
1. 两个顶点之间的路径数;
2. 两个顶点之间路径的长度;
3. 两个顶点之间的路径经过的顶点。
而相关性高的一对顶点一般具有如下特征:
1. 两个顶点之间有很多路径相连;
2. 连接两个顶点之间的路径长度都比较短;
3. 连接两个顶点之间的路径不会经过出度比较大的顶点。
我们可以举个例子来说明,如上图,用户 没有对《故乡的原风景》《偷功》有直接表达喜好,但是可以通过 , 两条路径为 的路径对《故乡的原风景》产生联系,同样也可以通过 , 两条路径为 的路径对《偷功》产生联系。那么,用户 与《偷功》之间的相关性要高于用户 与《故乡的原风景》,因而《偷功》在用户 的推荐列表中应该排在《故乡的原风景》之前。而 经过点的出度为 , 经过点的出度为 ,所以 对 的贡献要大于 。
下面内容参考《推荐系统实践》。基于上面的例子,产生了基于随机游走的
算法。假设要给用户
进行个性化推荐,可以从用户
对应的节点
开始在用户物品二分图上进行随机游走。游走到任何一个节点时,首先按照概率
决定是继续游走,还是停止这次游走并从
节点开始重新游走。如果决定继续游走,那么就从当前节点指向的节点中按照均匀分布随机选择一个节点作为游走下次经过的节点。这样,经过很多次随机游走后,每个物品节点被访问到的概率会收敛到一个数。最终的推荐列表中物品的权重就是物品节点的访问概率。
1. 实例分析
我们就是使用上面的例子来完整实现一个简单的推荐:
- 数据准备
实际使用的数据肯定不像我自己编的这么工整,具体数据具体分析。
def load_data(file_path):
records = []
f = open(file_path, "r", encoding="utf-8")
for line in f:
info = line.strip().split("\t")
records.append(info)
return records
获得标准数据集如下所示:
数据集: [['A', '英雄的黎明'], ['A', '最后的莫西干人'], ['B', '英雄的黎明'], ['B', '故乡的原风景'], ['B', '最后的莫西干人'], ['B', '偷功'], ['C', '最后的莫西干人'], ['C', '偷功']]
- 数据处理
有了数据集,我们就需要找出顶点及游走路径,我们这里需要获取用户顶点,歌曲顶点:
def calc_user_item(records): # 建立物品-用户的倒排列表
user_item = dict()
item_user = dict()
for user, item in records:
user_item.setdefault(user, dict())
user_item[user].setdefault(item, 0)
user_item[user][item] = 1 # 用户顶点
item_user.setdefault(item, dict())
item_user[item].setdefault(user, 0)
item_user[item][user] = 1 # 物品顶点
print("用户顶点: ", user_item)
print("物品顶点: ", item_user)
return user_item, item_user
用户顶点: {'A': {'英雄的黎明': 1, '最后的莫西干人': 1}, 'B': {'英雄的黎明': 1, '故乡的原风景': 1, '最后的莫西干人': 1, '偷功': 1}, 'C': {'最后的莫西干人': 1, '偷功': 1}}
物品顶点: {'英雄的黎明': {'A': 1, 'B': 1}, '最后的莫西干人': {'A': 1, 'B': 1, 'C': 1}, '故乡的原风景': {'B': 1}, '偷功': {'B': 1, 'C': 1}}
有了顶点,但是我们需要将其整理到一个顶点数据集中:
def initGraph(user_item, item_user):
G= dict()
G= dict(user_item, **item_user)
print("G: ", G)
return G
完整的顶点集合如下:
G: {'A': {'英雄的黎明': 1, '最后的莫西干人': 1}, 'B': {'英雄的黎明': 1, '故乡的原风景': 1, '最后的莫西干人': 1, '偷功': 1}, 'C': {'最后的莫西干人': 1, '偷功': 1}, '英雄的黎明': {'A': 1, 'B': 1}, '最后的莫西干人': {'A': 1, 'B': 1, 'C': 1}, '故乡的原风景': {'B': 1}, '偷功': {'B': 1, 'C': 1}}
- 算法实现
获取顶点集合后,我们数据处理算是完成,接下来就需要使用 来计算每个节点的访问概率,这里我设置 为 ,初始节点为 ,最大步数为 :
# G: 二分图 alpha:随机游走概率 root: 初始节点 max_step: 最大游走步数
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()}
for i, ri in G.items(): # i 是顶点。ri 是与其相连的顶点及其边的权重
for j, wij in ri.items(): # j 是 i 的连接顶点,wij 是权重
if j not in tmp:
tmp[j] = 0
tmp[j] += 0.6 * rank[i] / (1.0 * len(ri))
if j == root:
tmp[j] += 1 - alpha # 很多博主说这段放在这里会影响,我暂时没发现,希望清楚原因的可以帮我解惑
rank = tmp
rec = sorted(rank.items(),key = lambda x:x[1],reverse = True) # 将推荐歌曲按兴趣度排名
print("节点访问概率: ", rec)
return rank
不同顶点的访问概率为:
节点访问概率: [('A', 0.3618098988777181), ('最后的莫西干人', 0.12940492021276595), ('英雄的黎明', 0.11976304945054946), ('B', 0.07480053191489362), ('C', 0.032139569207388356), ('偷功', 0.020861950549450545), ('故乡的原风景', 0.011220079787234041)]
- 推荐
有了每个顶点的访问概率,即用户和歌曲的访问概率,我们就可以开始给用户推荐了,我们这里就给用户 推荐歌曲,因为顶点有用户顶点和歌曲顶点,所以我们去掉用户顶点和推荐用户本身已经感兴趣的顶点:
def Recommend(user, rank, user_item):
rec = []
for music in rank:
data = music[0]
rec.append(data)
for u, v in user_item.items(): # 移除用户顶点
for i in rec:
if i == u:
rec.remove(i)
for u, v in user_item[user].items(): # 移除用户已经标记过的歌曲
for i in rec:
if i == u:
rec.remove(i)
return rec
最终推荐结果为:
推荐物品: ['偷功', '故乡的原风景']
2. 算法改进
本章节内容摘抄自《推荐系统实践》。虽然
算法可以通过随机游走进行比较好的理论解释,但该算法在时间复杂度上有明显的缺点。因为在为每个用户进行推荐时,都需要在整个用户物品二分图上进行迭代,直到整个图上的每个顶点的
值收敛。这一过程的时间复杂度非常高,不仅无法在线提供实时推荐,甚至离线生成推荐结果也很耗时。
为了解决PersonalRank每次都需要在全图迭代并因此造成时间复杂度很高的问题,这里给出两种解决方案。第一种很容易想到,就是减少迭代次数,在收敛之前就停止。这样会影响最终的精度,但一般来说影响不会特别大。另一种方法就是从矩阵论出发,重新设计算法。对矩阵运算比较熟悉的读者可以轻松将
转化为矩阵的形式。令
为用户物品二分图的转移概率矩阵,即:
那么,迭代公式可以转化为:
因此,只需要计算一次
,这里 $
是稀疏矩阵。具体实现后续会继续。
3. 代码分析
基于图的推荐算法