参考:https://github.com/Lockvictor/MovieLens-RecSys/blob/master/usercf.py#L169
数据集
本文使用了MovieLens中的ml-100k小数据集,数据集的地址为:传送门
该数据集中包含了943个独立用户对1682部电影做的10000次评分。
首先看一下数据:
data = pd.read_csv('u.data', sep=' ', names=['user_id', 'item_id', 'rating', 'timestamp']) print(data)
完整代码
1 import numpy as np 2 import pandas as pd 3 import math 4 from collections import defaultdict 5 from operator import itemgetter 6 7 np.random.seed(1) 8 9 10 class UserCF(object): 11 12 def __init__(self): 13 self.train_set = {} 14 self.test_set = {} 15 self.movie_popularity = {} 16 17 self.tot_movie = 0 18 self.W = {} # 相似度矩阵 19 20 self.K = 20 # 最接近的K个用户 21 self.M = 10 # 推荐电影数 22 23 def split_data(self, data, ratio): 24 ''' 按ratio的比例分成训练集和测试集 ''' 25 for line in data.itertuples(): 26 user, movie, rating = line[1], line[2], line[3] 27 if np.random.random() < ratio: 28 self.train_set.setdefault(user, {}) 29 self.train_set[user][movie] = int(rating) 30 else: 31 self.test_set.setdefault(user, {}) 32 self.test_set[user][movie] = int(rating) 33 print('数据预处理完成') 34 35 def user_similarity(self): 36 ''' 计算用户相似度 ''' 37 movie_users = {} 38 for user, items in self.train_set.items(): 39 for movie in items.keys(): 40 if movie not in movie_users: 41 movie_users[movie] = set() 42 movie_users[movie].add(user) 43 if movie not in self.movie_popularity: # 用于后面计算新颖度 44 self.movie_popularity[movie] = 0 45 self.movie_popularity[movie] += 1 46 print('倒排表完成') 47 self.tot_movie = len(movie_users) # 用于计算覆盖率 48 49 C, N = {}, {} # C记录u,v之间给相同电影打分的数量, N记录用户打分的电影数量 50 for movie, users in movie_users.items(): 51 for u in users: 52 C.setdefault(u, defaultdict(int)) 53 N.setdefault(u, 0) 54 N[u] += 1 55 for v in users: 56 if u == v: 57 continue 58 C[u][v] += 1 59 60 train_user_num = len(self.train_set) # 训练集用户数 61 count = 1 62 for u, related_users in C.items(): 63 print(' 相似度计算进度:{:.2f}%'.format(count * 100 / train_user_num), end='') 64 count += 1 65 self.W.setdefault(u, {}) 66 for v, cuv in related_users.items(): 67 self.W[u][v] = float(cuv) / math.sqrt(N[u] * N[v]) 68 print(' 相似度计算完成') 69 70 def recommend(self, u): 71 ''' 通过与u最相似的K个用户推荐M部电影 ''' 72 rank = {} 73 user_movies = self.train_set[u] 74 for v, similarity in sorted(self.W[u].items(), key=itemgetter(1), reverse=True)[0:self.K]: 75 for movie, rating in self.train_set[v].items(): 76 if movie in user_movies: 77 continue 78 rank.setdefault(movie, 0) 79 rank[movie] += similarity * rating 80 return sorted(rank.items(), key=itemgetter(1), reverse=True)[0:self.M] 81 82 def evaluate(self): 83 ''' 评测算法 ''' 84 hit = 0 85 ret = 0 86 rec_tot = 0 87 pre_tot = 0 88 tot_rec_movies = set() # 推荐电影 89 for user in self.train_set: 90 test_movies = self.test_set.get(user, {}) 91 rec_movies = self.recommend(user) 92 for movie, pui in rec_movies: 93 if movie in test_movies.keys(): 94 hit += 1 95 tot_rec_movies.add(movie) 96 ret += math.log(1+self.movie_popularity[movie]) 97 pre_tot += self.M 98 rec_tot += len(test_movies) 99 precision = hit / (1.0 * pre_tot) 100 recall = hit / (1.0 * rec_tot) 101 coverage = len(tot_rec_movies) / (1.0 * self.tot_movie) 102 ret /= 1.0 * pre_tot 103 print('precision=%.4f' % precision) 104 print('recall=%.4f' % recall) 105 print('coverage=%.4f' % coverage) 106 print('popularity=%.4f' % ret) 107 108 109 if __name__ == '__main__': 110 data = pd.read_csv('u.data', sep=' ', names=['user_id', 'item_id', 'rating', 'timestamp']) 111 usercf = UserCF() 112 usercf.split_data(data, 0.7) 113 usercf.user_similarity() 114 usercf.evaluate()
结果
在不同的K值下运行的结果
相似度计算的改进
在现实中,很多人因为电影热门而去看它,此时也许这并不是他的兴趣所在,如果两个人同时看了相同的冷门电影,那么也许他们更有可能有更高的相似度。
对此,可以适当降低热门电影的加成比例,提高冷门电影的加成比例。
因此,只需对上述代码做此修改
C[u][v] += 1 / math.log(1 + len(users))
再重新进行评测,发现修改后在各项性能上都有所提高。