推荐系统实践 0x07 基于邻域的算法(2)
基于邻域的算法(2)
上一篇我们讲了基于用户的协同过滤算法,基本流程就是寻找与目标用户兴趣相似的用户,按照他们对物品喜好的对目标用户进行推荐,其中哪些相似用户的评分要带上目标用户与相似用户的相似度作为权重来计算。但是,基于用户的协同过滤算法存在一些弊端,如计算用户兴趣相似度矩阵将越来越困难,其运算时间复杂度和空间复杂度的增长和用户数的增长近似于平方关系,另外也很难对推荐结果进行解释。那么,这一篇我们继续来了解一下基于物品的协同过滤算法。
基于物品的协同过滤算法(ItemCF)
基于物品的协同过滤算法是大多数网站常用的推荐算法的基础。ItemCF不会利用物品的内容属性计算物品之间的相似度,而是分析用户的行为记录计算物品之间的相似度。那么,ItemCF主要分为两个步骤:
- 计算物品之间的相似度。
 - 根据物品的相似度和用户的历史行为给用户生成推荐列表。
 
物品相似度
我们可以用如下公式定义物品的相似度:
\]
\(N(i)\)是指喜欢物品\(i\)的用户数量,分子部分表示既喜欢物品\(i\)又喜欢物品\(j\)的用户有多少,整个相似度公式表示的是喜欢物品\(i\)的用户中,同时喜欢物品\(j\)的用户比例是多少。可以使用归一化之后的结果作为物品相似度。但是如果物品\(j\)很热门人人都喜欢,那么整个相似度就会变成1,这对于推荐冷门物品的推荐系统来说并不是好事情,所以我们对物品相似度公式进行改进。
\]
这个公式惩罚了热门物品\(j\)的权重,一定程度缓和了这个问题。同样的,我们在计算物品相似度的时候可以先建立一个用户-物品的倒排表,伪代码如下:
def ItemSimilarity(train):
    # calculate co-rated users between items
    C = dict()
    N = dict()
    for u, items in train.items():
        for i in users:
            N[i] += 1
            for j in users:
                if i == j:
                    continue
                C[i][j] += 1
    # finial similarity matrix W W = dict()
    for i, related_items in C.items():
        for j, cij in related_items.items():
            W[u][v] = cij / math.sqrt(N[i] * N[j])
    return W
用户对物品的兴趣
ItemCF虽然没有利用内容属性计算相似度,但是最后得到的结果仍然是内容上某种相似的,如同主演,同分类等等的电影。在得到物品相似度之后,我们用如下公式计算用户对物品的兴趣:
\]
这里\(N(u)\)是用户喜欢的物品的集合,\(S(j,K)\)是和物品\(j\)最相似的\(K\)个物品的集合,\(w_{ji}\)是物品\(j\)和\(i\)的相似度,\(r_{ui}\)是用户\(u\)对物品\(i\)的兴趣。
def Recommendation(train, user_id, W, K):
    rank = dict()
    ru = train[user_id]
    for i, pi in ru.items():
        for j, wj in sorted(W[i].items(), key=itemgetter(1),
                            reverse=True)[0:K]:
            if j in ru:
                continue
            rank[j] += pi * wj
    return rank
另外加就是用户活跃度对物品相似度产生的影响。一个不活跃的用户含有大量的感兴趣的物品,那么会产生稠密的物品相似度大矩阵,所以活跃用户对物品相似度的贡献应该小于不活跃的用户。那么公式修正为:
\]
跟基于用户的协同过滤的修正公式很像啊。
def ItemSimilarity(train):
    #calculate co-rated users between items C = dict()
    N = dict()
    for u, items in train.items():
        for i in users:
            N[i] += 1
            for j in users:
                if i == j:
                    continue
            C[i][j] += 1 / math.log(1 + len(items) * 1.0)
    #calculate finial similarity matrix W W = dict()
    for i,related_items in C.items():
        for j, cij in related_items.items()
            W[u][v] = cij / math.sqrt(N[i] * N[j])
    return W
还是得感谢@Magic-Bubble分享在github上代码,清晰易懂,省去我重复造轮子的时间。那么给出在MovieLens数据集上的实验代码:
# 导入包
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 ItemCF(train, K, N):
    '''
    :params: train, 训练数据集
    :params: K, 超参数,设置取TopK相似物品数目
    :params: N, 超参数,设置取TopN推荐物品数目
    :return: GetRecommendation, 推荐接口函数
    '''
    # 计算物品相似度矩阵
    sim = {}
    num = {}
    for user in train:
        items = train[user]
        for i in range(len(items)):
            u = items[i]
            if u not in num:
                num[u] = 0
            num[u] += 1
            if u not in sim:
                sim[u] = {}
            for j in range(len(items)):
                if j == i: continue
                v = items[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_item_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 item in train[user]:
            for u, _ in sorted_item_sim[item][:K]:
                if u not in seen_items:
                    if u not in items:
                        items[u] = 0
                    items[u] += sim[item][u]
        recs = list(sorted(items.items(), key=lambda x: x[1],
                           reverse=True))[:N]
        return recs
    return GetRecommendation
# 2. 基于改进的物品余弦相似度的推荐
def ItemIUF(train, K, N):
    '''
    :params: train, 训练数据集
    :params: K, 超参数,设置取TopK相似物品数目
    :params: N, 超参数,设置取TopN推荐物品数目
    :return: GetRecommendation, 推荐接口函数
    '''
    # 计算物品相似度矩阵
    sim = {}
    num = {}
    for user in train:
        items = train[user]
        for i in range(len(items)):
            u = items[i]
            if u not in num:
                num[u] = 0
            num[u] += 1
            if u not in sim:
                sim[u] = {}
            for j in range(len(items)):
                if j == i: continue
                v = items[j]
                if v not in sim[u]:
                    sim[u][v] = 0
                # 相比ItemCF,主要是改进了这里
                sim[u][v] += 1 / math.log(1 + len(items))
    for u in sim:
        for v in sim[u]:
            sim[u][v] /= math.sqrt(num[u] * num[v])
    # 按照相似度排序
    sorted_item_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 item in train[user]:
            for u, _ in sorted_item_sim[item][:K]:
                # 要去掉用户见过的
                if u not in seen_items:
                    if u not in items:
                        items[u] = 0
                    items[u] += sim[item][u]
        recs = list(sorted(items.items(), key=lambda x: x[1],
                           reverse=True))[:N]
        return recs
    return GetRecommendation
# 3. 基于归一化的物品余弦相似度的推荐
def ItemCF_Norm(train, K, N):
    '''
    :params: train, 训练数据集
    :params: K, 超参数,设置取TopK相似物品数目
    :params: N, 超参数,设置取TopN推荐物品数目
    :return: GetRecommendation, 推荐接口函数
    '''
    # 计算物品相似度矩阵
    sim = {}
    num = {}
    for user in train:
        items = train[user]
        for i in range(len(items)):
            u = items[i]
            if u not in num:
                num[u] = 0
            num[u] += 1
            if u not in sim:
                sim[u] = {}
            for j in range(len(items)):
                if j == i: continue
                v = items[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])
    # 对相似度矩阵进行按行归一化
    for u in sim:
        s = 0
        for v in sim[u]:
            s += sim[u][v]
        if s > 0:
            for v in sim[u]:
                sim[u][v] /= s
    # 按照相似度排序
    sorted_item_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 item in train[user]:
            for u, _ in sorted_item_sim[item][:K]:
                if u not in seen_items:
                    if u not in items:
                        items[u] = 0
                    items[u] += sim[item][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='../dataset/ml-1m/ratings.dat',
                 rt='ItemCF'):
        '''
        :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 = {
            'ItemCF': ItemCF,
            'ItemIUF': ItemIUF,
            'ItemCF-Norm': ItemCF_Norm
        }
    # 定义单次实验
    @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. ItemCF实验
M, N = 8, 10
for K in [5, 10, 20, 40, 80, 160]:
    cf_exp = Experiment(M, K, N, rt='ItemCF')
    cf_exp.run()
# 2. ItemIUF实验
M, N = 8, 10
K = 10  # 与书中保持一致
iuf_exp = Experiment(M, K, N, rt='ItemIUF')
iuf_exp.run()
# 3. ItemCF-Norm实验
M, N = 8, 10
K = 10  # 与书中保持一致
norm_exp = Experiment(M, K, N, rt='ItemCF-Norm')
norm_exp.run()
参考
推荐系统实践 0x07 基于邻域的算法(2)的更多相关文章
- 推荐系统实践 0x06 基于邻域的算法(1)
		
基于邻域的算法(1) 基于邻域的算法主要分为两类,一类是基于用户的协同过滤算法,另一类是基于物品的协同过滤算法.我们首先介绍基于用户的协同过滤算法. 基于用户的协同过滤算法(UserCF) 基于用户的 ...
 - 推荐系统实践 0x09 基于图的模型
		
用户行为数据的二分图表示 用户的购买行为很容易可以用二分图(二部图)来表示.并且利用图的算法进行推荐.基于邻域的模型也可以成为基于图的模型,因为基于邻域的模型都是基于图的模型的简单情况.我们可以用二元 ...
 - 推荐系统实践 0x05 推荐数据集MovieLens及评测
		
推荐数据集MovieLens及评测 数据集简介 MoiveLens是GroupLens Research收集并发布的关于电影评分的数据集,规模也比较大,为了让我们的实验快速有效的进行,我们选取了发布于 ...
 - 协同滤波 Collaborative filtering 《推荐系统实践》 第二章
		
利用用户行为数据 简介: 用户在网站上最简单存在形式就是日志. 原始日志(raw log)------>会话日志(session log)-->展示日志或点击日志 用户行一般分为两种: 1 ...
 - zz京东电商推荐系统实践
		
挺实在 今天为大家分享下京东电商推荐系统实践方面的经验,主要包括: 简介 排序模块 实时更新 召回和首轮排序 实验平台 简介 说到推荐系统,最经典的就是协同过滤,上图是一个协同过滤的例子.协同过滤主要 ...
 - 推荐系统实践 0x0b 矩阵分解
		
前言 推荐系统实践那本书基本上就更新到上一篇了,之后的内容会把各个算法拿来当专题进行讲解.在这一篇,我们将会介绍矩阵分解这一方法.一般来说,协同过滤算法(基于用户.基于物品)会有一个比较严重的问题,那 ...
 - 异常检测-基于孤立森林算法Isolation-based Anomaly Detection-1-论文学习
		
论文http://202.119.32.195/cache/10/03/cs.nju.edu.cn/da2d9bef3c4fd7d2d8c33947231d9708/tkdd11.pdf 1. INT ...
 - QT:用QSet储存自定义结构体的问题——QSet和STL的set是有本质区别的,QSet是基于哈希算法的,要求提供自定义==和qHash函数
		
前几天要用QSet作为储存一个自定义的结构体(就像下面这个程序一样),结果死活不成功... 后来还跑到论坛上问人了,丢脸丢大了... 事先说明:以下这个例子是错误的 #include <QtCo ...
 - 基于多重信号分类算法的DOA估计
		
原创博文,转载请注明出处 下面的论文是我的雷达处理的作业,拿来共享,不喜勿喷.由于公式编辑器的原因,无法复制公式,全部内容请点击. 基于多重信号分类算法的DOA估计 1引言 多重信号分类(MUSIC) ...
 
随机推荐
- 稳压二极管、肖特基二极管、静电保护二极管、TVS管
			
1.稳压二极管 正向导通电压跟普通二级管一样约为0.7v,反向状态下在临界电压之前截止,在达到临界电压的条件下会处于导通的状态,电压也不再升高,所以用在重要元器件上,起到稳压作用. 稳压二极管主要利用 ...
 - 本地文件r如何上传到github上
			
来源:http://www.cnblogs.com/shenchanghui/p/7184101.html 来源:http://blog.csdn.net/zamamiro/article/detai ...
 - git 移除远程仓库关联
			
1 git remote rm origin // 移除本地关联 2 git remote add origin git@github.com/example.git // 添加线上仓库 3 git ...
 - 《Clojure编程》笔记 第16章 Clojure与web
			
目录 背景简述 第16章 Clojure与web 16.1 术语 16.2 Clojure栈 16.3 基石:Ring 16.3.1 请求与应答 16.3.2 适配函数 16.3.3 处理函数 16. ...
 - C语言100题集合005-删除一维数组中所有相同的数,使之只剩一个
			
系列文章<C语言经典100例>持续创作中,欢迎大家的关注和支持. 喜欢的同学记得点赞.转发.收藏哦- 后续C语言经典100例将会以pdf和代码的形式发放到公众号 欢迎关注:计算广告生态 即 ...
 - 【SpringBoot】10.SpringBoot文件上传
			
SpringBoot整合Thymeleaf 1.创建Thymeleaf的入门项目 maven构建简单项目 修改pom文件添加thymeleaf的坐标 <!-- thymeleaf的坐标 --&g ...
 - C#练习题 if
			
提示用户输入用户名,然后再提示输入密码,如果用户名是"admin"并且密码是"888888",则提示正确,否则,如果用户名不是admin还提示用户用户名不存在, ...
 - 【linux】-Makefile简要知识+一个通用Makefile
			
目录 Makefile Makefile规则与示例 为什么需要Makefile Makefile样式 先介绍Makefile的两个函数 完善Makefile 通用Makefile的使用 通用的Make ...
 - gitlab - 解决访问 gitlab 网站出现 502 报错信息的问题
			
问题背景 访问 gitlab 网站,出现 502 解决方案 先查看运行 gitlab 容器的 id docker ps 运行命令 # 容器里启动服务 docker exec id gitlab-ctl ...
 - IP/TCP/UDP checsum
			
今天调试bug时, 忘了将原始的check_sum值reset,导致发包-抓包后发现.check-sum 错误. 来看一看check-sum:简单讲就是对要计算的数据,以16bit为单元进行累加,然后 ...