说明
1.首先是图的构建,这里使用二元组来存储一个二部图,在一个二元字典中存储用户和物品之间的交互图结构。
2.PersonalRank类包含训练和预测两个过程,这里的训练是训练出单个用户的 相关性二元组(通过图上的相关性关系挖掘得到),预测阶段是使用这一相关性二元组进行 指定潜在条件下的 top k推荐。
代码
# coding: utf-8 -*-
import pickle
import pandas as pd
class Graph:
'''
对图的 维护管理 构建部分。
这里是对 用户图结构 和 物品项图结果的构建, 但是 这里 只是创建字典吧, 用字典来存储图的架构,感觉不是这样的吧。
感觉主要是一个大的字典吧, 分别存储了 用户图 和物品项图(产生过交互的)。
其中 用户图 形式为(内部存储的形式):
user_{}: {'item_54272': 1, 'item_1': 1, 'item_33794': 1, 'item_69122': 1}
物品项图形式为(内部存储形式):
item_{}:{'user_288': 1, 'user_555': 1, 'user_151': 1, 'user_217': 1, 'user_314': 1}
以上为一个字典存放键值对,其实就相当于构建了一个字典吧。 这样就完成了 图的结点和 关联边的存储,可以简单看做二部图吧。
一些概念上面:根据用户与物品的相关性,对于相关性高的顶点有如下的定义:
(1)两个顶点之间有很多路径相连
(2)连接两个顶点之间的路径长度都比较短
(3)连接两个顶点之间的路径不会经过度比较大的顶点
'''
graph_path = 'data/prank.graph'
@classmethod
def _gen_user_graph(cls, user_id):
'''
获取目标用户二分图, 不计权重
:param frame: ratings数据
:param userID: 目标ID
:return: 二分图字典
'''
print('Gen graph user: {}'.format(user_id))
item_ids = list(set(cls.frame[cls.frame['UserID'] == user_id]['MovieID']))
graph_dict = {'item_{}'.format(item_id): 1 for item_id in item_ids}
print('_gen_user_graph:',graph_dict)
return graph_dict
@classmethod
def _gen_item_graph(cls, item_id):
'''
获取目标物品二分图, 不计权重
:param frame: ratings数据
:param userID: 目标ID
:return: 二分图字典
'''
print('Gen graph item: {}'.format(item_id))
user_ids = list(set(cls.frame[cls.frame['MovieID'] == item_id]['UserID']))
graph_dict = {'user_{}'.format(user_id): 1 for user_id in user_ids}
print('_gen_item_graph:', graph_dict)
return graph_dict
@classmethod
def gen_graph(cls):
"""
Gen graph.Each user,movie define as a node, and every movie rated by user means
that there is a edge between user and movie, edge weight is 1 simply.
初始化二分图
"""
file_path = 'data/ratings.csv'
cls.frame = pd.read_csv(file_path)
user_ids = list(set(cls.frame['UserID']))
item_ids = list(set(cls.frame['MovieID']))
cls.graph = {'user_{}'.format(user_id): cls._gen_user_graph(user_id) for user_id in user_ids}
for item_id in item_ids:
cls.graph['item_{}'.format(item_id)] = cls._gen_item_graph(item_id)
cls.save()
@classmethod
def save(cls):
f = open(cls.graph_path, 'wb')
pickle.dump(cls.graph, f)
f.close()
@classmethod
def load(cls):
f = open(cls.graph_path, 'rb')
graph = pickle.load(f)
f.close()
return graph
class PersonalRank:
'''
这里是对上面构建的 二部图结构的使用,基于随机游走的PersonalRank算法
随机游走迭代 输入: 二分图 alpha: 随机游走的概率 userID: 目标用户 iterCount: 迭代次数
参考链接:https://blog.csdn.net/sinat_33741547/article/details/53002524
'''
def __init__(self):
self.graph = Graph.load()
self.alpha = 0.6
self.iter_count = 20
self._init_model()
def _init_model(self):
"""
Initialize prob of every node, zero default.
"""
self.params = {k: 0 for k in self.graph.keys()}
def train(self, user_id):
"""
For target user, every round will start at that node, means prob will be 1.
And node will be updated by formula like:
for each node, if node j have edge between i:
prob_i_j = alpha * prob_j / edge_num_out_of_node_j
then prob_i += prob_i_j
alpha means the prob of continue walk.
这里对训练的理解,可以参考下 https://blog.csdn.net/sinat_33741547/article/details/53002524 此处说的 pageRank算法,通过
那个算法,来进一步的理解PersonalRank算法,这里一个基础的想法 是通过统计收敛的方法计算出 用户结点指向 商品项的概率。
感觉下面这个计算 累计概率的方式好难看懂啊,算是看明白了,这要还是基于图比较好理解这里随机游走的思想吧。
其中初始 params :{'user_1': 1, 'user_2': 0, 'user_3': 0, 'user_4': 0........}
graph.items()包含了所有商品图和用户 图的 点和连接边。
node, edges是指二部图中存储的结点和边。
next_node 是与边相关联的结点。
这里一个核心的思想就在下面这个式子,其思想可以参考pagerank思想,表示针对 与一个 起始结点 user_id=1 结点(代码初始设置self.params..=1的左右是后面计算游走图时候只对指定的起始结点有效)
tmp[next_node] += self.alpha * self.params[node] / len(edges) 【这里self.params[node]是个小trcik,这里是对单用户创造的图,所以这里之前已经限定了只有node为自己指定的用户时候,这个值才为1,其他都为0】
(式子主要体现对重要度的思考,在关联分值越小的地方,其作用越大。)
可以看到这里的核心就是这个 tmp[next_node] 的计算,用于计算next_node 所代表的项对于当前user_id=1的重要度,按照这种公式来思考,当在以往的交互中,对于user_id来接的商品边,商品边经过的次数越多,就会不断加大其概率。这样不断的去加大
从而可以使得用户边经常交互 的商品边的 权重不断增加,这就是随机游走的思想,在对以往的游走过程中,来加大一些权重、
因为不太好理解,才写了那么多,本质上就是利用二部图上的 关联关系, 找到从指定user_id出发下,经常会多次经过的 item_318,我们在交互
过程中经过的结点权重,越是经常经过(有一定的计算重要度方式),则给予其更高的重要度,这样就通过图的关系,找到潜在高关联的商品项了,
本质利用图上更复杂的表示能力,找到潜在高关联关系的商品项做推荐。 注意这里训练过程其实是构架 针对单个user 图的过程, 预测时候是
直接利用这个随机游走构建出的图按节点重要度做预测。
tmp 初始只有是从指定 user_id 扩展过来的点时tmp传播项才不为 0. user_id刚开始只传item,然后item也有值后,会作为传播源传给user,所以这就是为什么最后关联值既有 用户、也有商品的原因了
那这样通过几次完整的遍历游走之后,会进行不断的扩展传播,但是确定的是传染源就是 自己设定的user_id, 随机传播后,最后谁身上毒素多,说明其与user_id 关联就比较大,也就是在随机游走中,经过本身被多次游走到。
此外因为 游走重要度评估的不断成熟,那么传播源的作用是越来越递减的,这就是 1 - self.alpha不断减小的原因。
总体下来就是构建了用户的随机游走重要评估二元组,之后利用二元组做排序。
"""
self.params['user_{}'.format(user_id)] = 1
print('params:', self.params)
for count in range(self.iter_count): #这里是游走次数
print('Step {}...'.format(count))
tmp = {k: 0 for k in self.graph.keys()}
# edges mean all edge out of node
for node, edges in self.graph.items():
for next_node, _ in edges.items():
# every edge come in next_node update prob
tmp[next_node] += self.alpha * self.params[node] / len(edges)
# root node.
tmp['user_' + str(user_id)] += 1 - self.alpha
#print('params:', self.params)
#print('tmp:',tmp)
self.params = tmp
self.params = sorted(self.params.items(), key=lambda x: x[1], reverse=True)
print('self.params:',self.params)
self.save(user_id)
def predict(self, user_id, top_n=10):
"""
Return top n node without movie target user have been rated and other user.
这里的预测,就是对于 指定的 user 构建好的 二元组,选出 没有交互过的,潜在关联大的候选商品做推荐。
"""
self.load(user_id)
frame = pd.read_csv('data/ratings.csv')
item_ids = ['item_' + str(item_id) for item_id in list(set(frame[frame['UserID'] == user_id]['MovieID']))]
candidates = [(key, value) for key, value in self.params if key not in item_ids and 'user' not in key]
return candidates[:top_n]
def save(self, user_id):
f = open('data/prank_{}.model'.format(user_id), 'wb')
pickle.dump(self.params, f)
f.close()
def load(self, user_id):
f = open('data/prank_{}.model'.format(user_id), 'rb')
self.params = pickle.load(f)
f.close()