• 推荐系统实践 0x06 基于邻域的算法(1)


    基于邻域的算法(1)

    基于邻域的算法主要分为两类,一类是基于用户的协同过滤算法,另一类是基于物品的协同过滤算法。我们首先介绍基于用户的协同过滤算法。

    基于用户的协同过滤算法(UserCF)

    基于用户的协同过滤算法是最古老的算法了,它标志着推荐系统的诞生。当一个用户甲需要个性化推荐时,首先找到那些跟他兴趣相似的用户,然后把那些用户喜欢的,甲没有听说过的物品推荐给用户甲,那么这种方式就叫做基于用户的协同过滤算法。

    那么,这个算法包含两个步骤:

    1. 找到和目标用户兴趣相似的用户集合。
    2. 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户。

    我们用用户行为的相似度来表示兴趣的相似度。对于用户(u)和用户(v)(N(u))(N(v))表示各自有过正反馈的物品集合。那么我们用Jaccard公式表示用户(u)和用户(v)之间的兴趣相似度。

    [w_{uv}=frac{|N(u)cap N(v)|}{|N(u)cup N(v)|} ]

    另外也可以通过余弦相似度进行计算

    [w_{uv}=frac{|N(u)cap N(v)|}{sqrt{|N(u)||N(v)|}} ]

    余弦相似度的计算代码为

    def UserSimilarity(train):
        W = dict()
        for u in train.keys():
            for v in train.keys():
                if u == v:
                    continue
                W[u][v] = len(train[u] & train[v])
                W[u][v] /= math.sqrt(len(train[u]) * len(train[v]) * 1.0)
        return W
    

    如果这样去计算的话,在用户非常大的时候会非常耗时,因为很多用户之间并没有对相同的物品产生过行为,算法也把时间浪费在计算用户兴趣相似度上。那么我们可以对公式分子部分交集不为空的部分。

    建立物品到用户的倒排表,对于每个物品都保存对该物品产生过行为的用户列表。

    def UserSimilarity(train):
        # build inverse table for item_users
        item_users = dict()
        for u, items in train.items():
            for i in items.keys():
                if i not in item_users:
                    item_users[i] = set()
                item_users[i].add(u)
                #calculate co-rated items between users
        C = dict()
        N = dict()
        for i, users in item_users.items():
            for u in users:
                N[u] += 1
                for v in users:
                    if u == v:
                        continue
                    C[u][v] += 1
        # calculate finial similarity matrix W
        W = dict()
        for u, related_users in C.items():
            for v, cuv in related_users.items():
                W[u][v] = cuv / math.sqrt(N[u] * N[v])
        return W
    

    有了其他用户的对某个物品(i)感兴趣的评分,那么根据相似度可以计算出用户(u)对物品(i)的感兴趣评分为:

    [p(u,i) = sum_{vin S(u,K) cap N(i)}{w_{uv}r_{vi}} ]

    其中(S(u,K))是与用户(u)最相似的(K)个用户。因为使用的是单一行为的隐反馈数据,所以所有的评分都为1。另外还可以对用户的相似度进行改进,比如对冷门物品的兴趣更能反应他们的兴趣相似度。所以可以加上热门物品相似度的惩罚。

    [w_{uv}=frac{sum_{iin N(u)cap N(v)}frac{1}{log 1+|N(i)|}}{sqrt{|N(u)||N(v)|}} ]

    我们用上一篇介绍的MovieLens数据集,以及以前介绍的评测方式来把代码串起来,代码来自于参考里面的github,总体代码为:

    import random
    import math
    import time
    from tqdm import tqdm
    
    
    def timmer(func):
        def wrapper(*args, **kwargs):
            start_time = time.time()
            res = func(*args, **kwargs)
            stop_time = time.time()
            print('Func %s, run time: %s' %
                  (func.__name__, stop_time - start_time))
            return res
    
        return wrapper
    
    
    class Dataset():
        def __init__(self, fp):
            # fp: data file path
            self.data = self.loadData(fp)
    
        @timmer
        def loadData(self, fp):
            data = []
            for l in open(fp):
                data.append(tuple(map(int, l.strip().split('::')[:2])))
            return data
    
        @timmer
        def splitData(self, M, k, seed=1):
            '''
            :params: data, 加载的所有(user, item)数据条目
            :params: M, 划分的数目,最后需要取M折的平均
            :params: k, 本次是第几次划分,k~[0, M)
            :params: seed, random的种子数,对于不同的k应设置成一样的
            :return: train, test
            '''
            train, test = [], []
            random.seed(seed)
            for user, item in self.data:
                # 这里与书中的不一致,本人认为取M-1较为合理,因randint是左右都覆盖的
                if random.randint(0, M - 1) == k:
                    test.append((user, item))
                else:
                    train.append((user, item))
    
            # 处理成字典的形式,user->set(items)
            def convert_dict(data):
                data_dict = {}
                for user, item in data:
                    if user not in data_dict:
                        data_dict[user] = set()
                    data_dict[user].add(item)
                data_dict = {k: list(data_dict[k]) for k in data_dict}
                return data_dict
    
            return convert_dict(train), convert_dict(test)
    
    
    class Metric():
        def __init__(self, train, test, GetRecommendation):
            '''
            :params: train, 训练数据
            :params: test, 测试数据
            :params: GetRecommendation, 为某个用户获取推荐物品的接口函数
            '''
            self.train = train
            self.test = test
            self.GetRecommendation = GetRecommendation
            self.recs = self.getRec()
    
        # 为test中的每个用户进行推荐
        def getRec(self):
            recs = {}
            for user in self.test:
                rank = self.GetRecommendation(user)
                recs[user] = rank
            return recs
    
        # 定义精确率指标计算方式
        def precision(self):
            all, hit = 0, 0
            for user in self.test:
                test_items = set(self.test[user])
                rank = self.recs[user]
                for item, score in rank:
                    if item in test_items:
                        hit += 1
                all += len(rank)
            return round(hit / all * 100, 2)
    
        # 定义召回率指标计算方式
        def recall(self):
            all, hit = 0, 0
            for user in self.test:
                test_items = set(self.test[user])
                rank = self.recs[user]
                for item, score in rank:
                    if item in test_items:
                        hit += 1
                all += len(test_items)
            return round(hit / all * 100, 2)
    
        # 定义覆盖率指标计算方式
        def coverage(self):
            all_item, recom_item = set(), set()
            for user in self.test:
                for item in self.train[user]:
                    all_item.add(item)
                rank = self.recs[user]
                for item, score in rank:
                    recom_item.add(item)
            return round(len(recom_item) / len(all_item) * 100, 2)
    
        # 定义新颖度指标计算方式
        def popularity(self):
            # 计算物品的流行度
            item_pop = {}
            for user in self.train:
                for item in self.train[user]:
                    if item not in item_pop:
                        item_pop[item] = 0
                    item_pop[item] += 1
    
            num, pop = 0, 0
            for user in self.test:
                rank = self.recs[user]
                for item, score in rank:
                    # 取对数,防止因长尾问题带来的被流行物品所主导
                    pop += math.log(1 + item_pop[item])
                    num += 1
            return round(pop / num, 6)
    
        def eval(self):
            metric = {
                'Precision': self.precision(),
                'Recall': self.recall(),
                'Coverage': self.coverage(),
                'Popularity': self.popularity()
            }
            print('Metric:', metric)
            return metric
    
    
    # 1. 随机推荐
    def Random(train, K, N):
        '''
        :params: train, 训练数据集
        :params: K, 可忽略
        :params: N, 超参数,设置取TopN推荐物品数目
        :return: GetRecommendation,推荐接口函数
        '''
        items = {}
        for user in train:
            for item in train[user]:
                items[item] = 1
    
        def GetRecommendation(user):
            # 随机推荐N个未见过的
            user_items = set(train[user])
            rec_items = {k: items[k] for k in items if k not in user_items}
            rec_items = list(rec_items.items())
            random.shuffle(rec_items)
            return rec_items[:N]
    
        return GetRecommendation
    
    
    # 2. 热门推荐
    def MostPopular(train, K, N):
        '''
        :params: train, 训练数据集
        :params: K, 可忽略
        :params: N, 超参数,设置取TopN推荐物品数目
        :return: GetRecommendation, 推荐接口函数
        '''
        items = {}
        for user in train:
            for item in train[user]:
                if item not in items:
                    items[item] = 0
                items[item] += 1
    
        def GetRecommendation(user):
            # 随机推荐N个没见过的最热门的
            user_items = set(train[user])
            rec_items = {k: items[k] for k in items if k not in user_items}
            rec_items = list(
                sorted(rec_items.items(), key=lambda x: x[1], reverse=True))
            return rec_items[:N]
    
        return GetRecommendation
    
    
    # 3. 基于用户余弦相似度的推荐
    def UserCF(train, K, N):
        '''
        :params: train, 训练数据集
        :params: K, 超参数,设置取TopK相似用户数目
        :params: N, 超参数,设置取TopN推荐物品数目
        :return: GetRecommendation, 推荐接口函数
        '''
        # 计算item->user的倒排索引
        item_users = {}
        for user in train:
            for item in train[user]:
                if item not in item_users:
                    item_users[item] = []
                item_users[item].append(user)
    
        # 计算用户相似度矩阵
        sim = {}
        num = {}
        for item in item_users:
            users = item_users[item]
            for i in range(len(users)):
                u = users[i]
                if u not in num:
                    num[u] = 0
                num[u] += 1
                if u not in sim:
                    sim[u] = {}
                for j in range(len(users)):
                    if j == i: continue
                    v = users[j]
                    if v not in sim[u]:
                        sim[u][v] = 0
                    sim[u][v] += 1
        for u in sim:
            for v in sim[u]:
                sim[u][v] /= math.sqrt(num[u] * num[v])
    
        # 按照相似度排序
        sorted_user_sim = {k: list(sorted(v.items(), 
                                   key=lambda x: x[1], reverse=True)) 
                           for k, v in sim.items()}
    
        # 获取接口函数
        def GetRecommendation(user):
            items = {}
            seen_items = set(train[user])
            for u, _ in sorted_user_sim[user][:K]:
                for item in train[u]:
                    # 要去掉用户见过的
                    if item not in seen_items:
                        if item not in items:
                            items[item] = 0
                        items[item] += sim[user][u]
            recs = list(sorted(items.items(), key=lambda x: x[1],
                               reverse=True))[:N]
            return recs
    
        return GetRecommendation
    
    
    # 4. 基于改进的用户余弦相似度的推荐
    def UserIIF(train, K, N):
        '''
        :params: train, 训练数据集
        :params: K, 超参数,设置取TopK相似用户数目
        :params: N, 超参数,设置取TopN推荐物品数目
        :return: GetRecommendation, 推荐接口函数
        '''
        # 计算item->user的倒排索引
        item_users = {}
        for user in train:
            for item in train[user]:
                if item not in item_users:
                    item_users[item] = []
                item_users[item].append(user)
    
        # 计算用户相似度矩阵
        sim = {}
        num = {}
        for item in item_users:
            users = item_users[item]
            for i in range(len(users)):
                u = users[i]
                if u not in num:
                    num[u] = 0
                num[u] += 1
                if u not in sim:
                    sim[u] = {}
                for j in range(len(users)):
                    if j == i: continue
                    v = users[j]
                    if v not in sim[u]:
                        sim[u][v] = 0
                    # 相比UserCF,主要是改进了这里
                    sim[u][v] += 1 / math.log(1 + len(users))
        for u in sim:
            for v in sim[u]:
                sim[u][v] /= math.sqrt(num[u] * num[v])
    
        # 按照相似度排序
        sorted_user_sim = {k: list(sorted(v.items(), 
                                   key=lambda x: x[1], reverse=True)) 
                           for k, v in sim.items()}
    
        # 获取接口函数
        def GetRecommendation(user):
            items = {}
            seen_items = set(train[user])
            for u, _ in sorted_user_sim[user][:K]:
                for item in train[u]:
                    # 要去掉用户见过的
                    if item not in seen_items:
                        if item not in items:
                            items[item] = 0
                        items[item] += sim[user][u]
            recs = list(sorted(items.items(), key=lambda x: x[1],
                               reverse=True))[:N]
            return recs
    
        return GetRecommendation
    
    
    class Experiment():
        def __init__(self, M, K, N, fp='./ml-1m/ratings.dat',
                     rt='UserCF'):
            '''
            :params: M, 进行多少次实验
            :params: K, TopK相似用户的个数
            :params: N, TopN推荐物品的个数
            :params: fp, 数据文件路径
            :params: rt, 推荐算法类型
            '''
            self.M = M
            self.K = K
            self.N = N
            self.fp = fp
            self.rt = rt
            self.alg = {'Random': Random, 'MostPopular': MostPopular, 
                        'UserCF': UserCF, 'UserIIF': UserIIF}
    
        # 定义单次实验
        @timmer
        def worker(self, train, test):
            '''
            :params: train, 训练数据集
            :params: test, 测试数据集
            :return: 各指标的值
            '''
            getRecommendation = self.alg[self.rt](train, self.K, self.N)
            metric = Metric(train, test, getRecommendation)
            return metric.eval()
    
        # 多次实验取平均
        @timmer
        def run(self):
            metrics = {'Precision': 0, 'Recall': 0, 'Coverage': 0, 'Popularity': 0}
            dataset = Dataset(self.fp)
            for ii in range(self.M):
                train, test = dataset.splitData(self.M, ii)
                print('Experiment {}:'.format(ii))
                metric = self.worker(train, test)
                metrics = {k: metrics[k] + metric[k] for k in metrics}
            metrics = {k: metrics[k] / self.M for k in metrics}
            print('Average Result (M={}, K={}, N={}): {}'.format(
                                  self.M, self.K, self.N, metrics))
    
    
    # 1. random实验
    M, N = 8, 10
    K = 0  # 为保持一致而设置,随便填一个值
    random_exp = Experiment(M, K, N, rt='Random')
    random_exp.run()
    
    # 2. MostPopular实验
    M, N = 8, 10
    K = 0  # 为保持一致而设置,随便填一个值
    mp_exp = Experiment(M, K, N, rt='MostPopular')
    mp_exp.run()
    
    # 3. UserCF实验
    M, N = 8, 10
    for K in [5, 10, 20, 40, 80, 160]:
        cf_exp = Experiment(M, K, N, rt='UserCF')
        cf_exp.run()
    
    # 4. UserIIF实验
    M, N = 8, 10
    K = 80  # 与书中保持一致
    iif_exp = Experiment(M, K, N, rt='UserIIF')
    iif_exp.run()
    

    参考

    《推荐系统实践》(项亮等著) —— 代码实现

  • 相关阅读:
    org.tinygroup.tinydb-数据库开发组件
    org.tinygroup.database-数据库元数据定义
    org.tinygroup.databasebuinstaller-数据库结构及元数据自动创建
    org.tinygroup.dbrouter-数据库分区分表
    org.tinygroup.metadata-元数据定义
    org.tinygroup.jsqlparser-SQL解析器
    org.tinygroup.xmlparser-XML解析器
    四则运算程序扩展:将程序改为java语言,并允许用户输入,对输入结果进行验证
    课堂练习四: 返回一个整数数组中最大子数组的和。
    自动生成四则运算问题的测试
  • 原文地址:https://www.cnblogs.com/nomornings/p/14038255.html
Copyright © 2020-2023  润新知