基于邻域的算法(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_{v\in S(u,K) \cap N(i)}{w_{uv}r_{vi}}
\]

其中\(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)的更多相关文章

  1. 推荐系统实践 0x07 基于邻域的算法(2)

    基于邻域的算法(2) 上一篇我们讲了基于用户的协同过滤算法,基本流程就是寻找与目标用户兴趣相似的用户,按照他们对物品喜好的对目标用户进行推荐,其中哪些相似用户的评分要带上目标用户与相似用户的相似度作为 ...

  2. 推荐系统实践 0x09 基于图的模型

    用户行为数据的二分图表示 用户的购买行为很容易可以用二分图(二部图)来表示.并且利用图的算法进行推荐.基于邻域的模型也可以成为基于图的模型,因为基于邻域的模型都是基于图的模型的简单情况.我们可以用二元 ...

  3. 推荐系统实践 0x05 推荐数据集MovieLens及评测

    推荐数据集MovieLens及评测 数据集简介 MoiveLens是GroupLens Research收集并发布的关于电影评分的数据集,规模也比较大,为了让我们的实验快速有效的进行,我们选取了发布于 ...

  4. 协同滤波 Collaborative filtering 《推荐系统实践》 第二章

    利用用户行为数据 简介: 用户在网站上最简单存在形式就是日志. 原始日志(raw log)------>会话日志(session log)-->展示日志或点击日志 用户行一般分为两种: 1 ...

  5. zz京东电商推荐系统实践

    挺实在 今天为大家分享下京东电商推荐系统实践方面的经验,主要包括: 简介 排序模块 实时更新 召回和首轮排序 实验平台 简介 说到推荐系统,最经典的就是协同过滤,上图是一个协同过滤的例子.协同过滤主要 ...

  6. 推荐系统实践 0x0b 矩阵分解

    前言 推荐系统实践那本书基本上就更新到上一篇了,之后的内容会把各个算法拿来当专题进行讲解.在这一篇,我们将会介绍矩阵分解这一方法.一般来说,协同过滤算法(基于用户.基于物品)会有一个比较严重的问题,那 ...

  7. 异常检测-基于孤立森林算法Isolation-based Anomaly Detection-1-论文学习

    论文http://202.119.32.195/cache/10/03/cs.nju.edu.cn/da2d9bef3c4fd7d2d8c33947231d9708/tkdd11.pdf 1. INT ...

  8. QT:用QSet储存自定义结构体的问题——QSet和STL的set是有本质区别的,QSet是基于哈希算法的,要求提供自定义==和qHash函数

    前几天要用QSet作为储存一个自定义的结构体(就像下面这个程序一样),结果死活不成功... 后来还跑到论坛上问人了,丢脸丢大了... 事先说明:以下这个例子是错误的 #include <QtCo ...

  9. 基于多重信号分类算法的DOA估计

    原创博文,转载请注明出处 下面的论文是我的雷达处理的作业,拿来共享,不喜勿喷.由于公式编辑器的原因,无法复制公式,全部内容请点击. 基于多重信号分类算法的DOA估计 1引言 多重信号分类(MUSIC) ...

随机推荐

  1. Spring框架中获取连接池常用的四种方式

    1:DBCP数据源 DBCP类包位于 /lib/jakarta-commons/commons-dbcp.jar,DBCP是一个依赖Jakarta commons-pool对象池机制的数据库连接池,所 ...

  2. Swagger配置与使用

    问题:前后端分离时代的到来 前端需要测试后端数据 后端提供接口,实时更新接口的改动 一.Swagger简介 号称世界上最流行的api框架 Restful api文档在线自动生成工具-->api文 ...

  3. git每次提交代码都要设置账号密码的问题

    git config --global credential.helper store 待下一次提交代码的时候,输入了正确的用户名和密码,之后 就不需要输入用户名密码

  4. Luogu P4234 最小差值生成树

    题意 给定一个 \(n\) 个点 \(m\) 条边的有权无向图,求出原图的一棵生成树使得该树上最大边权与最小边权的差值最小. \(\texttt{Data Range:}1\leq n\leq 5\t ...

  5. Android Choreographer 源码分析

    Choreographer 的作用主要是配合 Vsync ,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整, ...

  6. PLC模拟量采集模块分辨率是什么意思?

    14位分辨率的模块(mo kuai)和16位分辨率的模块有什么不同的地方? 14位的模块最高位是符号位,我们用S表示符号位,那么这个模块的数值范围(fàn wéi)就是S111 1111 1111 1 ...

  7. hello world .net core 微服务框架 Viper

    1.Viper是什么? Viper 是.NET平台下的Anno微服务框架的一个示例项目.入门简单.安全.稳定.高可用.全平台可监控.底层通讯可以随意切换thrift grpc. 自带服务发现.调用链追 ...

  8. MYSQL中inner join、left join 和 right join的区别

    首先join连接是用来进行多表关联查询的,join连接方式有三种连接方式:inner join.left join 和 right join 1.inner join 可以简写成join,叫内连接,查 ...

  9. Python使用协程进行爬虫

    详情点我跳转 关注公众号"轻松学编程"了解更多. 1.协程 协程,又称微线程,纤程.英文名Coroutine. 协程是啥 ?? 首先我们得知道协程是啥?协程其实可以认为是比线程更小 ...

  10. 秒懂Dubbo接口(原理篇)

    引言 背景 单一应用架构 垂直应用架构 分布式服务架构 流动计算架构 为什么要用 Dubbo? 什么是分布式? 为什么要分布式? Dubbo 的架构 Dubbo 的架构图解 Dubbo 工作原理 Du ...