推荐系统 - 基于图随机游走的PersonalRank召回算法

说明

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()

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值