总体思路:
1.利用余弦相似度对两两用户计算相似度
1.1 建立物品-用户倒排表
左半部分为训练数据格式,ABCD等是用户,abc等是对应用户喜欢的物品
右半部分物品-用户倒排表,如对于物品a,喜欢它的有用户A和B
1.2 建立用户相似度矩阵
利用物品-用户倒排表,构建用户相似度矩阵,其中的值,如 matrix[A][B]表示用户A和用户B共同喜欢的电影的数量。
1.3 计算用户相似度
遍历用户相似度矩阵中所有的两两用户,根据两两用户共同喜欢的电影的数量,计算用户相似度
计算用户相似度的公式如下:
其中表示用户u与v的相似度,作为matrix[u][v]的值,
N(u)表示用户u增有过正反馈的物品集合,N(u)表示用户u增有过正反馈的物品集合。
例如:
或使用改进的用户相似度计算公式:
该公式惩罚了用户u和v共同喜欢的物品中热门物品对他们相似度的影响,以图书为例,如果两个用户都曾经买过《新华字典》,这丝毫不能说明他们的兴趣相似,因为绝大多数中国人小时候都买过《新华字典》。但如果两个用户都买过《数据挖掘导论》,那可以认为他们的兴趣比较相似,因为只有研究数据挖掘的人才会买这本书。换句话说,两个用户对冷门物品采取过同样的行为更能说明他们兴趣的相似度。
i表示用户u和用户v都有过正反馈的物品集合,N(i)表示对物品i有过正反馈的用户数
即分子部分表示 “用户u和v有正反馈的物品数”
2. 针对目标用户u,找到其最相似的K个用户,产生N个推荐
K表示与用户u兴趣相似的用户个数,N表示为用户u推荐的物品数
首先,对用户u,在用户相似度中找到与其相似度最高的K个用户
利用如下的公式计算用户u对物品i的感兴趣程度p(u, i):
其中,S(u, k)包含和用户u兴趣最接近的K个用户,N(i)是对物品i有过行为的用户集合,是用户u和用户v的相似度,表示用户v对i的兴趣,这里使用的是单一行为的隐反馈,即对物品有过用户行为即使=1。
该公式的意义在于:对于与用户u相似度接近的K个用户,遍历他们有过正反馈的物品,计算出用户u对每一个物品的感兴趣程度。
然后根据感兴趣程度由高到低确定N个推荐给用户u的物品。
3. 评测指标
将用户行为数据按照均匀分布随机划分为M份(如取M=8),挑选一份作为测试集,将剩下的M-1份作为训练集。为防止评测指标不是过拟合的结果,共进行M次实验,每次都使用不同的测试集。然后将M次实验测出的评测指标的平均值作为最终的评测指标。
3.1 召回率
对用户u推荐N个物品(记为R(u)),令用户u在测试集上喜欢的物品集合为T(u)
召回率描述有多少比例的用户-物品评分记录包含在最终的推荐列表中。
3.2 准确率
准确率描述最终的推荐列表中有多少比例是发生过的用户-物品评分记录
3.3 覆盖率
覆盖率反映了推荐算法发掘长尾的能力,覆盖率越高,说明推荐算法越能够将长尾中的物品推荐给用户。
分子部分表示实验中所有被推荐给用户的物品数目(集合去重),分母表示数据集中所有物品的数目
4. 实验部分
采用GroupLens提供的MovieLens数据集,http://www.grouplens.org/node/73 。本章使用中等大小的数据集,包含6000多用户对4000多部电影的100万条评分。该数据集是一个评分数据集,用户可以给电影评1-5分5个不同的等级。本文着重研究隐反馈数据集中TopN推荐问题,因此忽略了数据集中的评分记录。也就是说,TopN推荐的任务是预测用户会不会对某电影评分,而不是预测用户在准备对某部电影评分的前提下会给电影评多少分。
代码如下:
# coding = utf-8
# 基于用户的协同过滤推荐算法实现
import random
import math
from operator import itemgetter
class UserBasedCF():
''''''''''初始化相关参数'''
def __init__(self):
# 找到与目标用户兴趣相似的20个用户,为其推荐10部电影
self.n_sim_user = 80
self.n_rec_movie = 10
# 将数据集划分为训练集和测试集
self.trainSet = {} #字典
self.testSet = {}
# 用户相似度矩阵
self.user_sim_matrix = {} #使用典中典模拟矩阵
self.movie_count = 0
self.fenzi = {}
print('Similar user number = %d' % self.n_sim_user)
print('Recommneded movie number = %d' % self.n_rec_movie)
'''读文件得到“用户-电影”数据'''
def get_dataset(self, filename, pivot=0.875):
trainSet_len = 0
testSet_len = 0
random.seed()
for line in self.load_file(filename):
user, movie, rating, timestamp = line.split('::')
if random.random() < pivot: #训练集的数量约为75%
self.trainSet.setdefault(user, {}) #相当于trainSet.get(user),若该键不存在,则设trainSet[user] = {},典中典
#键中键:形如{'1': {'1287': '2.0', '1953': '4.0', '2105': '4.0'}, '2': {'10': '4.0', '62': '3.0'}}
#用户1看了id为1287的电影,打分2.0
self.trainSet[user][movie] = rating
trainSet_len += 1
else: #测试集数量约为25%
self.testSet.setdefault(user, {})
self.testSet[user][movie] = rating
testSet_len += 1
# if random.randint(0, 7) == 1: #书上的方法,M=8,一份为测试集,剩下为训练集,未注释的是github上方法,效果差不多
# self.testSet.setdefault(user, {})
# self.testSet[user][movie] = rating
# testSet_len += 1
# else:
# self.trainSet.setdefault(user, {}) #相当于trainSet.get(user),若该键不存在,则设trainSet[user] = {},典中典
#
# #键中键:形如{'1': {'1287': '2.0', '1953': '4.0', '2105': '4.0'}, '2': {'10': '4.0', '62': '3.0'}}
# self.trainSet[user][movie] = rating
# trainSet_len += 1
print('Split trainingSet and testSet success!')
print('TrainSet = %s' % trainSet_len)
print('TestSet = %s' % testSet_len)
'''读文件,返回文件的每一行'''
def load_file(self, filename):
with open(filename, 'r') as f:
for i, line in enumerate(f):
if i == 0: # 去掉文件第一行的title
continue
yield line.strip('\r\n')
print('Load %s success!' % filename)
'''计算用户之间的相似度(改进版 User-IIF)'''
def improved_userSimilarity(self):
#构建“电影-用户”倒排索引
print("Building movie-user table...")
movie_uesr = {}
for user, movies in self.trainSet.items():
for movie in movies:
if movie not in movie_uesr:
movie_uesr[movie] = set()
movie_uesr[movie].add(user)
print("Build movie-user table success!")
#建立用户相似度矩阵
self.movie_count = len(movie_uesr)
print('Total movie number = %d' % self.movie_count)
print('Build user co-rated movies matrix...')
for movie, users in movie_uesr.items():
for u in users:
for v in users:
if u == v:
continue
# self.fenzi.setdefault(u, {})
# self.fenzi[u].setdefault(v, 0)
# self.fenzi[u][v] += 1 / math.log(1 + len(users))
self.user_sim_matrix.setdefault(u, {})
self.user_sim_matrix[u].setdefault(v, 0)
self.user_sim_matrix[u][v] += 1 / math.log(1 + len(users))
print('Build user co-rated movies matrix success!')
# 计算相似性
print('Calculating user similarity matrix ...')
for u, related_users in self.user_sim_matrix.items():
for v, count in related_users.items(): # count表示用户u和v都看过电影数
# 计算用户相似度
self.user_sim_matrix[u][v] = count / math.sqrt(len(self.trainSet[u]) * len(self.trainSet[v]))
print('Calculate user similarity matrix success!')
'''计算用户之间的相似度 UserCF'''
def calc_user_sim(self):
# 构建“电影-用户”倒排索引
# key = movieID, value = list of userIDs who have seen this movie
print('Building movie-user table ...')
movie_user = {} #电影-用户倒排索引表
# .items()方法,该方法将返回所有键值对,并将其保存在一个元组列表(列表中的元素为元组)中:
#对于键中键,外层已是元组列表,里层还是字典的形式:[('1', {'1129': '2.0'}), ('2', {'39': '5.0', '110': '4.0'})]
for user, movies in self.trainSet.items(): #对每一个用户和他看过的电影们
for movie in movies: #对该用户看过的每一步电影
if movie not in movie_user:
movie_user[movie] = set() #字典中的集合,索引为电影,里层集合内容为用户们
movie_user[movie].add(user)
print('Build movie-user table success!')
print(movie_user)
self.movie_count = len(movie_user) #电影-用户倒排索引表的长度
print('Total movie number = %d' % self.movie_count)
print('Build user co-rated movies matrix ...') #建立用户相似度矩阵W
for movie, users in movie_user.items(): #取出键值对,每一部电影movie对应着观看过它的一群users
for u in users: #对该电影的每一种用户组合u-v
for v in users:
if u == v:
continue
self.user_sim_matrix.setdefault(u, {})
self.user_sim_matrix[u].setdefault(v, 0)
self.user_sim_matrix[u][v] += 1
print('Build user co-rated movies matrix success!')
# 计算相似性
print('Calculating user similarity matrix ...')
for u, related_users in self.user_sim_matrix.items():
for v, count in related_users.items(): #count表示用户u和v都看过电影数
#计算用户相似度
self.user_sim_matrix[u][v] = count / math.sqrt(len(self.trainSet[u]) * len(self.trainSet[v]))
print('Calculate user similarity matrix success!')
'''针对目标用户U,找到其最相似的K个用户,产生N个推荐'''
def recommend(self, user):
K = self.n_sim_user #与目标用户兴趣相似的用户个数
N = self.n_rec_movie #为其推荐电影数
rank = {}
watched_movies = self.trainSet[user]
# v=similar user, wuv=similar factor 相似度
for v, wuv in sorted(self.user_sim_matrix[user].items(), key=itemgetter(1), reverse=True)[0:K]: #按维度1(用户相似度)降序排列,取前K个
for movie, rvi in self.trainSet[v].items(): #遍历v看过的电影,及v对该电影的兴趣程度
if movie in watched_movies: #如果user已看过这部电影,跳过
continue
rank.setdefault(movie, 0)
rank[movie] += float(wuv) * float(rvi) #计算user对电影movie的感兴趣程度
return sorted(rank.items(), key=itemgetter(1), reverse=True)[0:N] #返回user感兴趣程度最高的N部电影
'''产生推荐并通过准确率、召回率和覆盖率进行评估'''
def evaluate(self):
print("Evaluation start ...")
N = self.n_rec_movie
# 准确率和召回率
hit = 0 #推荐命中数
rec_count = 0
test_count = 0
# 覆盖率
all_rec_movies = set()
for i, user, in enumerate(self.trainSet): #对训练集中每一个用户
test_movies = self.testSet.get(user, {}) #拿出典中典,即user看过的电影及对它们的打分
rec_movies = self.recommend(user) #给user推荐的topN部电影及计算出的user对它们感兴趣的程度
# print(rec_movies)
for movie, w in rec_movies: #查看推荐的电影是否在测试集上喜欢的电影中出现,如果是,就+1
if movie in test_movies:
hit += 1
all_rec_movies.add(movie) #被推荐的电影的集合(用于计算覆盖率)
rec_count += N #推荐给所有训练集中用户的电影数的加和
test_count += len(test_movies) #所有的用户看过的电影的加和
precision = hit / (1.0 * rec_count)
recall = hit / (1.0 * test_count)
coverage = len(all_rec_movies) / (1.0 * self.movie_count) #movie_count:电影_用户倒排索引表的长度,即所有电影的数目
print('precisioin=%.4f\trecall=%.4f\tcoverage=%.4f' % (precision, recall, coverage))
if __name__ == '__main__':
rating_file = 'ratings2.csv'
userCF = UserBasedCF()
userCF.get_dataset(rating_file)
#userCF.calc_user_sim()
userCF.improved_userSimilarity()
userCF.evaluate()
结果如下: