最近,因为导师项目需要,花了几天时间学习了项亮的《推荐系统实践》,并用python实现了书上的Item Collaborative Filtering即基于物品的协同过滤算法,发现很多博客在算法的代码的实现上说得很笼统,而且项亮的书中关于协同过滤的代码实现写得又很零碎,故写此文总结。
什么是协同过滤
协同过滤是推荐系统中最经典和常用的算法,其核心思想就是它的名字:利用所有用户的历史行为数据,用户通过不断地和网站互动,使推荐列表能够不断过滤掉用户不感兴趣的物品,从而越来越满足需求。
协同过滤分为两大类:
- 基于用户的协同过滤(UserCF):当要给目标用户进行推荐时,先找出与他相似的其他用户,再从那些用户喜欢的项目中找出目标用户没有看过的项目推荐给他。
- 基于项目的协同过滤(ItemCF):先找出目标用户历史观看列表里与之相似的其他项目,再对这些相似项目进行排序并生成最后的推荐列表。
评分预测还是TopN推荐?
评分预测和TopN推荐是推荐系统两种衡量预测准确度的指标,大多数的推荐研究都是基于评分预测来展开的,主要是因为从事这方面早期研究的组织GroupLens主要就是针对电影评分进行的,而且Netflix等推荐算法的大赛也主要是在解决评分预测问题,然而,评分高就是好的推荐吗?当然不是,有时候用户给出好的评分并不代表他最想看它或者购买它。
部分代码实现
- 记载数据集并划分训练集与测试集
def get_dataset(self, filename, pivot=0.75):
trainSet_len = 0
testSet_len = 0
for line in self.load_file(filename):
user, movie, rating, timestamp = line.split(',')
if(random.random() < pivot):
self.trainSet.setdefault(user, {})
self.trainSet[user][movie] = rating
trainSet_len += 1
else:
self.testSet.setdefault(user, {})
self.testSet[user][movie] = rating
testSet_len += 1
print('划分训练集与测试集成功!')
print('训练集长 = %s' % trainSet_len)
print('测试集长 = %s' % testSet_len)
这里的pivot(枢轴)因子将数据集以3:1分成了训练集与数据集。
- 计算电影之间的相似度并生成相似矩阵
def calc_movie_sim(self):
#建立movies_popular字典
for user, movies in self.trainSet.items():
for movie in movies:
#若该movie没在movies_popular字典中,则把其插入字典并赋值为0,否则+1,最终的movie_popular字典键为电影名,值为所有用户总的观看数
if movie not in self.movie_popular:
self.movie_popular[movie] = 0
else:
self.movie_popular[movie] += 1
self.movie_count = len(self.movie_popular)
print("训练集中电影总数 = %d" % self.movie_count)
for user, movies in self.trainSet.items():
for m1 in movies:
for m2 in movies:
if m1 == m2:
continue
#下面三步的作用是:分别将每个用户看过的每一部电影与其他所有电影的联系值置1,若之后又有用户同时看了两部电影, 则+1
self.movie_sim_matrix.setdefault(m1, {})
self.movie_sim_matrix[m1].setdefault(m2, 0)
self.movie_sim_matrix[m1][m2] += 1
print("建立电影的相似矩阵成功!")
# print("矩阵进行相似计算前movieId=1的一行为:")
# print(self.movie_sim_matrix['1'])
# 计算电影之间的相似性
for m1, related_movies in self.movie_sim_matrix.items():
for m2, count in related_movies.items():
# 注意0向量的处理,即某电影的用户数为0
if self.movie_popular[m1] == 0 or self.movie_popular[m2] == 0:
self.movie_sim_matrix[m1][m2] = 0
else:
self.movie_sim_matrix[m1][m2] = count / math.sqrt(self.movie_popular[m1] * self.movie_popular[m2])
print('计算电影的相似矩阵成功!')
# print("电影相似矩阵中movieId=736的一行:")
# print(self.movie_sim_matrix['736'])
- 生成推荐列表
def recommend(self, user):
K = int(self.n_sim_movie)
N = int(self.n_rec_movie)
rank = {}
watched_movies = self.trainSet[user]
for movie, rating in watched_movies.items():
#对目标用户每一部看过的电影,从相似电影矩阵中取与这部电影关联值最大的前K部电影,若这K部电影用户之前没有看过,则把它加入rank字典中,其键为movieid名,其值(即推荐度)为w(相似电影矩阵的值)与rating(用户给出的每部电影的评分)的乘积
for related_movie, w in sorted(self.movie_sim_matrix[movie].items(), key=itemgetter(1), reverse=True)[:K]:
if related_movie in watched_movies:
continue
rank.setdefault(related_movie, 0)
#计算推荐度
rank[related_movie] += w * float(rating)
return sorted(rank.items(), key=itemgetter(1), reverse=True)[:N]
4.评估算法
def evaluate(self):
N = int(self.n_rec_movie)
reuserN = input("请输入参与评估的用户数量:(总用户数为457)")
reuserN = int(reuserN)
# 准确率和召回率
hit = 0
rec_count = 0
test_count = 0
# 覆盖率
all_rec_movies = set()
for user,m in list(self.trainSet.items())[:reuserN]:
test_moives = self.testSet.get(user, {})
rec_movies = self.recommend(user)
print("用户 %s 的电影推荐列表为:" % user)
self.precommend(rec_movies)
#注意,这里的w与上面recommend的w不要一样,上面的w是指计算出的相似电影矩阵的权值,而这里是这推荐字典rank对应的推荐度
for movie, w in rec_movies:
if movie in test_moives:
hit += 1
all_rec_movies.add(movie)
rec_count += N
test_count += len(test_moives)
precision = hit / (1.0 * rec_count)
recall = hit / (1.0 * test_count)
coverage = len(all_rec_movies) / (1.0 * self.movie_count)
print('准确率=%.4f\t召回率=%.4f\t覆盖率=%.4f' % (precision, recall, coverage))
最后,附上ItemCF数据集及完整代码地址