基本原理
-
核心思想
找到和用户A相似的其他用户,向A推荐这些其他用户喜欢的物品。
-
用户相似度:
w u v = ∣ N ( u ) ⋂ N ( v ) ∣ ∣ N ( u ) ∣ ∣ N ( v ) ∣ w_{uv} = \frac{|N(u)\bigcap N(v)|}{\sqrt{|N(u)||N(v)|}} wuv=∣N(u)∣∣N(v)∣∣N(u)⋂N(v)∣
N(u)代表的是用户u喜欢的物品集合,上述相似度衡量的是用户u和用户v的喜欢物品的重叠程度。
如果要统计所有用户两两之间的相似程度,时间复杂度为n(n-1)/2, 因而这里通过空间换时间的方法,建立倒排表后计算每两个用户的公共物品个数, 如下图所示:
w u , v = ∑ i ∈ N ( u ) ⋂ N ( v ) 1 l o g ( 1 + H o t i ) ∣ N ( u ) ∣ ∣ N ( v ) ∣ w_{u,v} = \frac{\sum_{i \in N(u)\bigcap N(v)}\frac{1}{log(1+Hot_i)}}{\sqrt{|N(u)||N(v)|}} wu,v=∣N(u)∣∣N(v)∣∑i∈N(u)⋂N(v)log(1+Hoti)1
-
用户对物品的兴趣
p ( u , i ) = ∑ v ∈ S ( u , K ) ⋂ N ( i ) w u , v r v i p(u, i) = \sum_{v \in S(u, K)\bigcap N(i)}w_{u,v} r_{vi} p(u,i)=v∈S(u,K)⋂N(i)∑wu,vrvi
这里S(u,K)代表的是和用户u最相似的K个用户的集合,N(i)代表的是喜欢物品i的用户的集合,rvi代表的是用户v对物品i的兴趣程度,为了方便起见,这里都记为1。
具体实现
代码逻辑
-
构建训练-测试集
原始数据集中每条数据的组织格式如下:user_0: item_00, play_seconds, timestamp; item_01, play_seconds, timestamp; ... user_1: item_10, play_seconds, timestamp; item_11, play_seconds, timestamp; ... ... user_N: item_M0, play_seconds, timestamp; item_M1, play_seconds, timestamp; ...
构建数据集的思路为:遍历每个user的播放历史列表中的每个item,遍历过程中生成一个[0,1]之间的随机数random,如果random小于pivot,则将item放入训练集中,否则放入测试集中,其余步骤(timestamp)可以忽略,因为这是和自己业务相关的代码。
def load_train_test_set(self, file_path, pivot=0.75): if not os.path.exists(file_path): print("no exists file") exit(-1) fn = open(file_path, "r") user_item_pair_count = 0 reach_user_item_pair_max_num = False user_ctnr = set() item_ctnr = set() for line in fn: if reach_user_item_pair_max_num: break data = line.strip().split(":") if len(data) < 2: continue user = data[0] user_ctnr.add(user) item_timestamp_list = data[1].split(";") for item_timestamp in item_timestamp_list: item_timestamp_elem_list = item_timestamp.split(",") if len(item_timestamp_elem_list) < 3: continue item = item_timestamp_elem_list[0] item_ctnr.add(item) if random.random() < pivot: self.train_set.setdefault(user, {}) self.train_set[user][item] = 1.0 else: self.test_set.setdefault(user, {}) self.test_set[user][item] = 1.0 user_item_pair_count += 1 if user_item_pair_count >= self.user_item_pair_max_num: reach_user_item_pair_max_num = True break print("load_train_test_set succ, user_count: {}, item_count: {}".format(len(user_ctnr), len(item_ctnr)))
-
计算用户相似度矩阵
构建用户相似度矩阵有以下4步:- 获取训练集中每个item的热度
- 构建物品到用户的倒排索引表
- 根据倒排索引表构建初级用户相似度矩阵,这里的初级用户相似度矩阵是指不考虑用户相似度矩阵的分母所得到的相似度矩阵
- 根据初级用户相似度矩阵和物品热度构建终极用户相似度矩阵
def calc_user_sim(self): # 获取item的popular度 movie_popular = {} for _, movies in self.train_set.items(): for movie in movies: if movie not in movie_popular: movie_popular.setdefault(movie, 0) movie_popular[movie] += 1 # 构建物品-用户倒排索引表 movie_user = {} for user, movies in self.train_set.items(): for movie in movies: if movie not in movie_user: movie_user[movie] = set() movie_user[movie].add(user) self.movie_count = len(movie_user) # 根据倒排索引表构建初级用户相似度矩阵 for movie, users in movie_user.items(): for u in users: 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.0 / np.log(1.0 + movie_popular[movie]) # 根据初级用户相似度矩阵和物品热度构建终极用户相似度矩阵 for u, related_users in self.user_sim_matrix.items(): for v, wuv_raw in related_users.items(): self.user_sim_matrix[u][v] = \ wuv_raw / math.sqrt(len(self.train_set[u]) * len(self.train_set[v])) print("calc_user_sim done")
这里个人总结一下为什么倒排索引能够加速计算的原因,这里假如有M个item,N个user,
由于用户的行为矩阵大多比较稀疏,即M个item中有用户行为的可能就只有M/5。如果直接计算所有用户之间的相似度,时间复杂度为 O ( M ∗ N 2 ) O(M*N^2) O(M∗N2)(用户两两之间计算相似度的复杂度为 N 2 N^2 N2,每个用户为一个M维的行为向量);引入倒排索引后,针对有用户行为的物品(M/5),其时间复杂度为 O ( M / 5 ∗ N 2 ) O(M/5*N^2) O(M/5∗N2),因而比直接计算用户相似度的时间复杂度要低。
-
针对某一输入用户,推荐资源
基本原理是对于输入用户,根据用户相似度矩阵找到最相似的前K个用户,将这K个用户看过的item放在一起,取综合评分(rating*用户相似度)最高的前N个item,推荐给输入用户def recommend(self, user): K = self.n_sim_user N = self.n_rec_movie rank = {} if user not in self.train_set: return [] watched_movies = self.train_set[user] if user not in self.user_sim_matrix: return [] user_sim_vec = sorted(self.user_sim_matrix[user].items(), key=itemgetter(1), reverse=True)[:K] for v, wuv in user_sim_vec: for movie, rating in self.train_set[v].items(): if movie in watched_movies: continue rank.setdefault(movie, 0) rank[movie] += wuv * rating return sorted(rank.items(), key=itemgetter(1), reverse=True)[:N]
-
构建评价函数
针对训练集的每一个用户,在测试集中找到用户看过的item作为标签,调用recommend函数对用户进行推荐,并计算平均的准确率+召回率+覆盖率对算法进行评价
def evaluate(self): hit = 0 rec_count = 0 test_count = 0 all_rec_movies = set() for user in self.train_set: test_movies = self.test_set.get(user, {}) rec_movies = self.recommend(user) for movie, w in rec_movies: if movie in test_movies: hit += 1 all_rec_movies.add(movie) rec_count += len(rec_movies) 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) print("precision is: {}, recall is {}, coverage is: {}".format(precision, recall, coverage)) return precision, recall, coverage
改进措施
实验思路
如果两个用户都喜欢一个item,但两个用户对这个item喜欢的时间点相差很大,说明两个用户在这个item粒度上确实是有差异的,有理由相信喜欢这个item的时间点差距越大,两个用户在这个item粒度上就越不相似,因而对于用户相似度计算有如下改进,
t
u
i
t_{ui}
tui代表的是用户v对物品i的喜好:
w
u
v
=
∑
i
∈
N
(
u
)
⋂
N
(
v
)
1
1
+
α
∣
t
u
i
−
t
v
i
∣
∣
N
(
u
)
∣
⋃
∣
N
(
v
)
∣
w_{uv} = \frac{\sum_{i\in N(u) \bigcap N(v)}\frac{1}{1+\alpha |t_{ui} - t{vi}|}}{\sqrt{|N(u)|\bigcup|N(v)|}}
wuv=∣N(u)∣⋃∣N(v)∣∑i∈N(u)⋂N(v)1+α∣tui−tvi∣1
预测阶段,将输入用户的相邻用户看过的物品都拿出来,计算用户对物品的喜好时将时间因子考虑在内,如下所示:
p
(
u
,
i
)
=
∑
v
∈
S
(
u
,
K
)
w
u
v
r
v
i
1
1
+
α
(
t
0
−
t
v
i
)
p(u, i) = \sum_{v\in S(u, K)}w_{uv}r_{vi}\frac{1}{1+\alpha(t_0-t_{vi})}
p(u,i)=v∈S(u,K)∑wuvrvi1+α(t0−tvi)1
优缺点
优点
- 速度快:倒排索引表的建立导致计算用户相似度矩阵的速度变快
- 较热门:由于给用户推荐的结果是从用户的相似用户看过的物品中挑选的,因而更倾向于给用户推荐热门物品
- 新物品快热:一旦某个用户对新物品有行为,就会将该物品推荐给该用户的邻居并迅速扩散
- 在线inference速度快:离线训练得到用户最喜爱的topN物品,作为词典挂载到线上,线上需要做任何计算操作
缺点
- 新用户慢热:不能对新用户进行很好地个性化推荐
- 缺乏个性化:无法在段时间内实现某用户的长尾相似物品推荐
- 不适用于获取反馈较困难场景
总结
ucf相对于icf来说偏探索,因为其基本原理是推荐target用户的相似用户看过的item,这本身引入很多不确定性,从而给用户推荐一些他没有看过的但系统认为他可能会喜欢的东西。