基于邻域的算法(2)

上一篇我们讲了基于用户的协同过滤算法,基本流程就是寻找与目标用户兴趣相似的用户,按照他们对物品喜好的对目标用户进行推荐,其中哪些相似用户的评分要带上目标用户与相似用户的相似度作为权重来计算。但是,基于用户的协同过滤算法存在一些弊端,如计算用户兴趣相似度矩阵将越来越困难,其运算时间复杂度和空间复杂度的增长和用户数的增长近似于平方关系,另外也很难对推荐结果进行解释。那么,这一篇我们继续来了解一下基于物品的协同过滤算法。

基于物品的协同过滤算法(ItemCF)

基于物品的协同过滤算法是大多数网站常用的推荐算法的基础。ItemCF不会利用物品的内容属性计算物品之间的相似度,而是分析用户的行为记录计算物品之间的相似度。那么,ItemCF主要分为两个步骤:

  1. 计算物品之间的相似度。
  2. 根据物品的相似度和用户的历史行为给用户生成推荐列表。

物品相似度

我们可以用如下公式定义物品的相似度:

\[w_{ij}=\frac{|N(i)\cap N(j)|}{|N(i)|}
\]

\(N(i)\)是指喜欢物品\(i\)的用户数量,分子部分表示既喜欢物品\(i\)又喜欢物品\(j\)的用户有多少,整个相似度公式表示的是喜欢物品\(i\)的用户中,同时喜欢物品\(j\)的用户比例是多少。可以使用归一化之后的结果作为物品相似度。但是如果物品\(j\)很热门人人都喜欢,那么整个相似度就会变成1,这对于推荐冷门物品的推荐系统来说并不是好事情,所以我们对物品相似度公式进行改进。

\[w_{ij}=\frac{|N(i)\cap N(j)|}{\sqrt{|N(i)||N(j)|}}
\]

这个公式惩罚了热门物品\(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虽然没有利用内容属性计算相似度,但是最后得到的结果仍然是内容上某种相似的,如同主演,同分类等等的电影。在得到物品相似度之后,我们用如下公式计算用户对物品的兴趣:

\[p_{uj}=\sum_{i\in N(u)\cap S(j,K)}w_{ji}r_{ui}
\]

这里\(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

另外加就是用户活跃度对物品相似度产生的影响。一个不活跃的用户含有大量的感兴趣的物品,那么会产生稠密的物品相似度大矩阵,所以活跃用户对物品相似度的贡献应该小于不活跃的用户。那么公式修正为:

\[w_{ij}=\frac{\sum_{u\in N(i)\cap N(j)}\frac{1}{\log 1+|N(u)|}}{\sqrt{|N(i)||N(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 / 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)的更多相关文章

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

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

  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. 利用数据库拿shell的一些姿势

    0x01.利用MySQL命令导出getshell 利用条件: 1.拥有网站的写入权限 2.Secure_file_priv参数为空或者为指定路径 3.知道网站的绝对路径 方法: 通过into outf ...

  2. LWJGL3的内存管理

    LWJGL3的内存管理 LWJGL3 (Lightweight Java Game Library 3),是一个支持OpenGL,OpenAl,Opengl ES,Vulkan等的Java绑定库.&l ...

  3. 什么是 session 和 cookie

    cookie 大家应该都熟悉,比如说登录某些网站一段时间后,就要求你重新登录:再比如有的同学很喜欢玩爬虫技术,有时候网站就是可以拦截住你的爬虫,这些都和 cookie 有关.如果你明白了服务器后端对于 ...

  4. 【CF1443F】Identify the Operations 题解

    原题链接 题意简介 建议去原题看.这题意我表达不清楚. 大概就是给你一个 n 的排列,现在要求你进行 m 次操作. 每次操作,你会在现有的排列中删去一个数,然后选择其左边或右边的一个与之相邻的数加入 ...

  5. Oracle初始——第一天

    一.数据库分类 1.小型数据库:access.foxbase 2.中型数据库:informix.sql server.mysql 3.大型数据库:sybase.db2.oracle 二.注释 1./* ...

  6. [游记] 2020ZJOI 爆零记

    DAY 0 凌晨一点打完CF,身心俱疲,由于头痛和困意原本可做的E没做出来,懊悔地睡下,竟然又过了一个多小时才睡着? 早上醒来,感觉身体轻飘飘的,("我真的睡过觉了吗")--听说打 ...

  7. 寻找性能更优秀的动态 Getter 和 Setter 方案

    反射获取 PropertyInfo 可以对对象的属性值进行读取或者写入,但是这样性能不好.所以,我们需要更快的方案. 方案说明 就是用表达式编译一个 Action<TObj,TValue> ...

  8. 直播APP源码是如何实现音视频同步的

    1.  音视频同步原理 1)时间戳 直播APP源码音视频同步主要用于在音视频流的播放过程中,让同一时刻录制的声音和图像在播放的时候尽可能的在同一个时间输出. 解决直播APP源码音视频同步问题的最佳方案 ...

  9. Qt基础之菜单栏

    本篇介绍Qt菜单栏相关操作,分为三部分:1.菜单栏相关的类介绍:2.系统菜单的生成和响应:3.弹出菜单的生成和响应:菜单栏通常只有以QMainWindow为基类的程序中才用到,以QWidget为基类的 ...

  10. Docker - 解决同步容器与主机时间报错:Error response from daemon: Error processing tar file(exit status 1): invalid symlink "/usr/share/zoneinfo/UTC" -> "../usr/share/zoneinfo/Asia/Shanghai"

    问题背景 这里讲解了如何同步容器和主机的时间:https://www.cnblogs.com/poloyy/p/13967532.html 其中使用方法二 docker cp /etc/localti ...