推荐系统实践 0x06 基于邻域的算法(1)
基于邻域的算法(1)
基于邻域的算法主要分为两类,一类是基于用户的协同过滤算法,另一类是基于物品的协同过滤算法。我们首先介绍基于用户的协同过滤算法。
基于用户的协同过滤算法(UserCF)
基于用户的协同过滤算法是最古老的算法了,它标志着推荐系统的诞生。当一个用户甲需要个性化推荐时,首先找到那些跟他兴趣相似的用户,然后把那些用户喜欢的,甲没有听说过的物品推荐给用户甲,那么这种方式就叫做基于用户的协同过滤算法。
那么,这个算法包含两个步骤:
- 找到和目标用户兴趣相似的用户集合。
- 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户。
我们用用户行为的相似度来表示兴趣的相似度。对于用户\(u\)和用户\(v\),\(N(u)\)和\(N(v)\)表示各自有过正反馈的物品集合。那么我们用Jaccard公式表示用户\(u\)和用户\(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\)的感兴趣评分为:
\]
其中\(S(u,K)\)是与用户\(u\)最相似的K个用户。因为使用的是单一行为的隐反馈数据,所以所有的评分都为1。
总体代码:
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()
参考
推荐系统实践 0x06 基于邻域的算法(1)的更多相关文章
- 推荐系统实践 0x07 基于邻域的算法(2)
		基于邻域的算法(2) 上一篇我们讲了基于用户的协同过滤算法,基本流程就是寻找与目标用户兴趣相似的用户,按照他们对物品喜好的对目标用户进行推荐,其中哪些相似用户的评分要带上目标用户与相似用户的相似度作为 ... 
- 推荐系统实践 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) ... 
随机推荐
- CyclicBarrier(循环栅栏)
			CyclicBarrier public class CyclicBarrierDemo { public static void main(String[] args) { CyclicBarrie ... 
- 图解git,用手绘图带你理解git中分支的原理和应用
			大家好,今天我们来聊git当中一个非常非常重要的特性,就是branch. git branch可以说是git当中最重要的概念了,甚至没有之一.因为git最重要的使用场景就是协同开发,大家一起在一个项目 ... 
- Vue基础(2)
			fetch与axios请求数据 fetch基本语法: fetch(url,{parmas}).then(res=> res.json() //返回promise对象 ).then(data=& ... 
- 微信小程序获取高宽uniapp
			代码片段 <template> <view> <view class="text" id="w">补充文字</view ... 
- uniapp微信小程序canvas绘图插入网络图片不显示
			网络图片缓存 在uni中wx可以用uni代替 无区别: 先把要插入的网络图片缓存(getImageInfo); let context = uni.createCanvasContext('first ... 
- numpy的统计分析
			一.排序 间接排序(argsort,lexsort) 根据一个或多个数据集进行排序 1.Sort() --对数值直接进行排序 a.一维排序 b.二维排序 c.axis的认知 2.argsort() - ... 
- 没有磁盘空间 No space left on device
			INSTALL 的解释文件 帮助文件 这里的 pytorch=1.0.1 torchvision=0.2.2 cudatoolkit=9.0,这个ATSS可以运行. 这里最好能够查看一下cuda的版本 ... 
- 深入探索Spring Data JPA, 从Repository 到  Specifications 和 Querydsl
			数据访问层,所谓的CRUD是后端程序员的必修课程,Spring Data JPA 可以让我们来简化CRUD过程,本文由简入深,从JPA的基本用法,到各种高级用法. Repository Spring ... 
- 【SpringBoot】01.创建Springboot项目及启动器
			创建Springboot项目及启动器 1.创建一个简单maven项目 SpringBoot2.0以下需要使用JDK1.7 ,2.0以上使用JDK1.8 如果需要修改JDK的版本需要打开pom文件: & ... 
- 21 Ajax
			21 Ajax AJAX,Asynchronous JavaScript and XML(异步的 JavaScript 和 XML), 是与在不重新加载整个页面的情况下,与服务器交换数据并更新部分网页 ... 
