概述
最近在做一个有关图书推荐系统的项目,因此就涉及到了协同过滤算法。自己主要负责基于用户的协同过滤算法(User-CF)的实现,因此阅读了部分书籍与文章,也基于组员爬取的豆瓣图书评论数据,参考了一个User-CF的源码做了一个简单的实现。
本文将首先简单介绍一下最基础的基于用户的协同过滤算法,再介绍基于皮尔逊相关度以及豆瓣图书评论数据的User-CF的实现。
简单介绍
基于用户的协同过滤算法(User-CF)属于基于领域的算法。
基于用户的协同过滤算法主要包括两个步骤。
- 找到和目标用户兴趣相似的用户集合。
- 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户
相似度算法
步骤一的关键就是计算两个用户之间的相似度,以下列举两种比较常见的相似度计算方法。
给定用户u和用户v,令N(u)表示用户u曾经有过正反馈的物品集合,令N(v)为用户v曾经有过正反馈的物品集合。
- Jaccard公式
- 余弦相似度
另外还有一种基于向量的相似度算法,也是在代码实现中将使用的相似度计算方法。具体的关于Pearson相关系数的介绍会在下文中阐述。
- Pearson相关系数
关于这三种相似度之间的差别和选择标准,自己也还没有仔细去研究。之后有时间的话会补充。
计算举例
计算单独的两个用户之间的相似度
下面将简单介绍利用余弦相似度计算方法得到两个用户之间的相似度。
计算举例例如,用户A对物品{a,b,d}有过行为,用户B对物品{a,c}有过行为,利用余弦相似度公式计算用户A和用户B的兴趣相似度为:
A和B之间的相似度同理,我们可以算出用户A和用户C、D的相似度:
A和C、D之间的相似度计算用户两两之间的相似度矩阵
对两两用户都利用余弦相似度计算相似度。这种方法的时间复杂度是O(N^2)。耗时很大。
因此我们需要建立一张物品到用户之间的倒排表,这样就能排除对根本没有任何联系的用户之间的相似度计算。
再根据倒排表计算共同评分过的物品的矩阵。
如下图所示
计算共同评分过的物品的矩阵矩阵中的每一个数值都是余弦相似度中的分子部分,然后将分子除以分母可以得到最终的用户兴趣相似度。
即可以将上图中的共同评分过的物品的矩阵转换为用户之间的相似度矩阵,且只用计算非0的部分。
例如,计算A与B的用户相似度。用户A和B的矩阵中的值为1,即他们共同交集的物品为1。A总共评分过的物品为3,B总共评分过的物品为2。根据余弦相似度算法,得出相似度为
计算结果筛选出K个与目标用户最相似的用户
得到了用户之间的相似度后,算法会给用户推荐和他兴趣最相似的K个用户喜欢的物品
以下公式度量了算法中用户u对物品i的感兴趣程度
用户u对物品i的感兴趣程度其中,S(u,K)包含和用户u兴趣最相近的K个用户,N(i)是对物品i有过行为的用户集合,Wuv是用户u和用户v的兴趣相似度,rvi为1
举例
对目标用户A进行推荐。选取K=3,用户A对物品c,e没有过行为,因此可以把这两个物品推荐给用户A。根据算法,用户A对物品c,e的兴趣是
用户A对物品c,e的兴趣基于豆瓣图书评论数据的简单实现
评论数据格式介绍
自己对评论数据作了一个简单的清洗,数据格式如下:
图书名+用户名+评分信息+评分时间
中间以"\t"
作为分隔符
部分图书数据如下图所示
部分图书数据使用Pearson相关系数计算用户相似度
之前介绍的协同过滤算法并没有引进评分的概念,引进评分之后,就不能简单地利用集合去计算了,而是应该利用向量去计算。
Pearson相关系数将两个用户共同评分的n个项目看做一组向量,计算两个用户在这n个项目上评分的相关性,减去用户平均评分是基于用户评分尺度的考量,公式如下:
Pearson相关系数其中Iuv
是用户u和v都评过分的项目的集合,ru
是用户u所有评分的平均分
计算用户u对物品i评分的预测
得到用户相似度后,接下来就是根据目标用户u的近邻用户得到u对于物品i的评分预测,公式如下:
用户u对物品i的评分预测评分预测越高,目标用户u喜欢该物品的概率也就越高。
最终按照评分预测的高低可得到最适合推荐给用户u的物品列表,在这个实例里也就是图书列表。
Python实现
导入数据
导入的原始数据为训练集(trainSet),利用该训练集生成图书-用户倒排表(bookUser),再利用倒排表生成用户-用户共同评分过的图书矩阵(u2u)
# encoding=utf-8
from math import sqrt
import codecs
def loadData():
# 训练集
trainSet = {}
# 看过该本书的所有用户集合
bookUser = {}
# 用户-用户共有书籍矩阵
u2u = {}
TrainFile = '/Users/cjl/Desktop/comments.txt'
# 加载训练集
# trainset {"userName", {"bookName, rating"}}
# bookUser {"bookName", ["username1", "username2", ...]} 即为物品-用户倒排表
for line in codecs.open(TrainFile, "r", "utf-8"):
(bookName, userName, rating, timestamp) = line.strip().split("\t")
trainSet.setdefault(userName, {})
trainSet[userName].setdefault(bookName, float(rating))
bookUser.setdefault(bookName, [])
bookUser[bookName].append(userName.strip())
# 生成用户-用户共有书籍矩阵
for b in bookUser.keys():
# 遍历特定的书籍中所有的用户
for u in bookUser[b]:
u2u.setdefault(u, {})
for n in bookUser[b]:
if u != n:
u2u[u].setdefault(n,[])
u2u[u][n].append(b)
return trainSet, u2u
简单测试导入的数据以及生成的u2u是否正确
测试导入数据计算一个用户的平均评分
def getAverageRating(user, trainSet):
average = (sum(trainSet[user].values())*1.0) / len(trainSet[user].keys())
return average
简单测试
简单测试计算用户相似度
遍历u2u,利用皮尔逊相关系数的计算,将共同评分过的书籍矩阵转换为两个用户之间的相似度矩阵
# 计算用户相似度
def getUserSim(u2u, trainSet):
userSim = {}
# 计算用户相似度
# 对每个用户u
for u in u2u.keys():
if u != '':
# 将用户u加入userSim中设为key,该用户对应一个字典
userSim.setdefault(u, {})
# 获取用户u对电影的平均评分
average_u_rate = getAverageRating(u, trainSet)
# 对与用户u相关的每个用户n
for n in u2u[u].keys():
if n != '':
# 将用户n加到用户u的字典中
userSim[u].setdefault(n,0)
# 获取用户n对电影的平均评分
average_n_rate = getAverageRating(n, trainSet)
# 皮尔逊相关系数的分子部分
part1 = 0
# 皮尔逊相关系数的分母的一部分
part2 = 0
# 皮尔逊相关系数的分母的一部分
part3 = 0
# 对用户u和用户n的共有的每个电影
for m in u2u[u][n]:
part1 += (trainSet[u][m] - average_u_rate) * (trainSet[n][m] - average_n_rate) * 1.0
part2 += pow(trainSet[u][m] - average_u_rate, 2) * 1.0
part3 += pow(trainSet[n][m] - average_n_rate, 2) * 1.0
part2 = sqrt(part2)
part3 = sqrt(part3)
# 若分母为0,相似度为0
if part2 == 0 or part3 == 0:
userSim[u][n] = 0
else:
userSim[u][n] = part1 / (part2 * part3)
return userSim
简单测试
输出相似用户寻找最近邻用户并生成推荐结果
遍历trainSet中的所有用户,生成一个评分预测pred
# 寻找用户最近邻并生成推荐结果
def getRecommendations(N, trainSet, userSim):
pred = {}
# 对每个用户
for user in trainSet.keys():
# 生成空预测表
pred.setdefault(user, {})
# 获取该用户评过分的书籍
interacted_items = trainSet[user].keys()
# 获取该用户的平均评分
average_u_rate = getAverageRating(user, trainSet)
userSimSum = 0
# 选取N个最相似的用户
# lambda x:x[1] 根据用户相似度进行比较
if user.strip() != '':
simUser = sorted(userSim[user.strip()].items(), key=lambda x: x[1], reverse=True)[0:N]
# 遍历相似用户的用户名和相似度
for n, sim in simUser:
# 得到该近邻用户的平均评分
average_n_rate = getAverageRating(n, trainSet)
# 对该用户所有近邻用户相似度求和
userSimSum += sim
# 遍历该近邻用户的所有评分书籍和具体评分
for m, nrating in trainSet[n].items():
# 如果这本书该用户已经看过,则跳过
if m in interacted_items:
continue
# 否则将这本书以及这本书的推荐指数加到这名用户的推荐列表中
else:
pred[user].setdefault(m, 0)
pred[user][m] += (sim * (nrating - average_n_rate))
# 遍历这名用户推荐列表中的所有书籍,更新评分预测
for m in pred[user].keys():
if userSimSum == 0:
pred[user][m] = average_u_rate
else:
pred[user][m] = average_u_rate + (pred[user][m] * 1.0) / userSimSum
return pred
简单测试
输出推荐书籍参考内容
书籍:《推荐系统实践》
网友评论