协同过滤
简介
协同过滤(Collaborative Filtering)是推荐系统知识体系里基础中的基础,虽然现在看来简单的协同过滤已经很古老和过时了,尤其UserCF,但它们仍然是学习推荐系统的必经之路。我主要参考了《推荐系统实践》和《Recommender Systems-An Introduction》两本书(书上部分公式和代码是有问题的),部分摘录,部分自己的想法,代码都自己实现过一遍。旨在为后续对推荐系统的学习打下一点基础。
一、基于用户的协同过滤 UserCF
UserCF的基本步骤包含下面两步:
(1)找到和目标用户兴趣相似的用户集合
(2)将这个集合中用户喜欢的且目标用户没有听说过的物品推荐给目标用户
1.1 如何衡量用户兴趣相似度
常见相似性度量
衡量相似度常用的方法有Pearson相关系数、Jaccard相似度、余弦距离等。
用户
u
u
和用户,
N(u)
N
(
u
)
表示用户
u
u
曾有过正面反馈(例如浏览、购买)的物品集合,表示用户
u
u
对物品的反馈指标(例如浏览次数、评分),
r¯u
r
¯
u
表示用户对所有物品的平均反馈(例如平均评分)。
Pearson相关系数
Jaccard相似度
余弦相似度
例如三个用户的浏览历史分别如下
用户 | 浏览物品列表 |
---|---|
Allen | a,b,d |
Ben | a,c |
Copper | b,e |
Denny | c,d,e |
计算用户Allen与其他三人的余弦相似度,发现Allen与Ben和Copper都比较相似。
物品-用户倒排表
直接用上面的公式计算,会有许多不必要的运算,比如两个用户的浏览记录可能没有交集,这样的用户之间根本不需要计算相似度。解决这个问题的方法是先构造物品-用户倒排表,然后用它计算相似度。
所谓物品-用户倒排表,就是以物品为索引,列出浏览过该物品的用户。上面这个例子的物品-用户倒排表如下:
物品 | 发生过浏览的用户列表 |
---|---|
a | Allen, Ben |
b | Allen, Copper |
c | Ben, Denny |
d | Allen, Denny |
e | Copper, Denny |
建立物品-用户倒排表,可以很快地构造出矩阵 C(u,v)=|N(u)∩N(v)| C ( u , v ) = | N ( u ) ∩ N ( v ) | 。具体地,遍历每个物品,把用户列表中的两两用户对应的 C(u,v) C ( u , v ) 加1即可,具体如下:
- | Allen | Ben | Copper | Denny |
---|---|---|---|---|
Allen | 0 | 1 | 1 | 1 |
Ben | 1 | 0 | 0 | 1 |
Copper | 1 | 0 | 0 | 1 |
Denny | 1 | 1 | 1 | 0 |
然后只需要将矩阵 C(u,v) C ( u , v ) 非零的元素除以对应的 |N(u)||N(v)|−−−−−−−−−−√ | N ( u ) | | N ( v ) | 就得到了相似度矩阵。
用户余弦相似度的python实现
由于用户的浏览可能非常稀疏,因此这里所有的矩阵都用字典实现,完全不存储0值。
# 计算用户之间的余弦相似度
def UserSimilarity(user_items):
# 构造物品-用户倒排表
item_users = {}
for user, item_set in user_items.iteritems():
for k in item_set:
if k not in item_users:
item_users[k] = set()
item_users[k].add(user)
# 有共同浏览记录的用户的重合度
C = {} # C(u,v)
N = {} # N(u), N(v)
for item, user_set in item_users.iteritems():
for u in user_set:
N[u] = 0 if u not in N else N[u] # 初始化
C[u] = {} if u not in C else C[u] # 初始化
N[u] += 1
for v in user_set:
C[u][v] = 0 if v not in C[u] else C[u][v] # 初始化
if(u!=v):
C[u][v] += 1
# 计算最终的相似度矩阵 W
W = {} # W(u,v)
for u, related_users in C.iteritems():
W[u] = {} if u not in W else W[u] # 初始化
for v, Cuv in related_users.iteritems():
if Cuv!=0:
W[u][v] = 0 if v not in W[u] else W[u][v] # 初始化
W[u][v] = Cuv / np.sqrt(N[u] * N[v])
return W
测试用例:
# 测试用例
user_items = {"Allen": set(['a','b','d']),
"Ben": set(['a','c']),
"Copper":set(['b','e']),
"Denny": set(['c','d','e'])}
# 计算用户余弦相似度
W = UserSimilarity(user_items)
# 从稀疏表达转矩阵
result = np.zeros((4,4))
users = ['Allen','Ben','Copper','Denny']
for u, related_users in W.iteritems():
for v, w in related_users.iteritems():
result[users.index(u)][users.index(v)] = w
print result
测试结果:
[[0. 0.40824829 0.40824829 0.33333333]
[0.40824829 0. 0. 0.40824829]
[0.40824829 0. 0. 0.40824829]
[0.33333333 0.40824829 0.40824829 0. ]]
1.2 如何给用户做推荐
当得到用户之间的相似度之后,UserCF会给用户推荐和他最相似的
K
K
个用户喜欢的物品,具体地,会衡量目标用户关于物品
i
i
的兴趣:
这里 S(u,K) S ( u , K ) 表示和用户 u u 最接近的个用户, N(i) N ( i ) 表示对物品 i i 有过行为的用户集合,所以的含义就是“和 u u 很相似且对物品有过行为的用户”。
然而,这种推荐方法存在覆盖度和流行度上的trade-off。所谓覆盖度,就是推荐的物品是否覆盖用户真正感兴趣的物品,而流行度,指物品在所有用户之中的热门程度。 K K 值调得过大,就会偏向于参考更多人的兴趣,很容易偏向热门物品,从而减少对长尾物品的推荐,热门固然是用户感兴趣的,但我们希望去挖掘一些冷门物品对用户的吸引力。
用python实现为用户推荐物品
# 预测用户对于他没有发生过交互的物品的兴趣
def Recommand(user, user_items, W, K):
rank = {}
interacted_items = user_items[user] # 用户user已经发生过交互的物品
for v, wuv in sorted(W[user].iteritems(), key=lambda x:x[1], reverse=True)[:K]:
for i, rvi in user_items[v].iteritems():
if not i in interacted_items: # 只推荐用户没有发生过交互的物品
rank[i] = 0 if i not in rank else rank[i]
rank[i] += wuv * rvi
return rank
测试用例
# 续上面的测试用例
rank = Recommand("Allen", user_items, W, K=3)
print rank
测试结果
{'c': 0.7415816237971964, 'e': 0.7415816237971964}
即用户Allen对于他没有浏览过的物品和物品 e e 的兴趣都是0.7416.
1.3 改进的用户相似度与推荐
前面提到热门物品带来的问题,现在举个例子。以书为例,学生A和B都买过《自然辨证法》,这并不能说明他们兴趣相似,因为《自然辨证法》是必修课,不得不买。但是如果他们都买了《推荐系统》,那可以认为他们兴趣相似,因为基本上只有做推荐系统方向的人才会买这本书。因此,两个用户对冷门物品的兴趣更能反映他们之间的相似性。
这篇论文提出了下面这种补偿方式 User-IIF(Inverse Item Frequency),和余弦相似度比较,只改动了分子。可以看到求和项中每一项都类似于tf-idf中的idf,对于特定的物品,如果与它发生过交互的用户越多,说明它越热门,则相似度受到的惩罚越大。
用python实现User-IIF
def UserSimilarity_IIF(user_items):
# 构造物品-用户倒排表
item_users = {}
for user, item_set in user_items.iteritems():
for k in item_set:
if k not in item_users:
item_users[k] = set()
item_users[k].add(user)
# 有共同浏览记录的用户的重合度
C = {} # C(u,v)
N = {} # N(u), N(v)
for item, user_set in item_users.iteritems():
for u in user_set:
N[u] = 0 if u not in N else N[u]
C[u] = {} if u not in C else C[u]
N[u] += 1
for v in user_set:
C[u][v] = 0 if v not in C[u] else C[u][v]
if(u!=v):
C[u][v] += 1 / np.log(1 + len(user_set))
# 计算最终的相似度矩阵 W
W = {} # W(u,v)
for u, related_users in C.iteritems():
W[u] = {} if u not in W else W[u] # 初始化
for v, Cuv in related_users.iteritems():
if Cuv != 0:
W[u][v] = 0 if v not in W[u] else W[u][v] # 初始化
W[u][v] = Cuv / np.sqrt(N[u] * N[v])
return W
测试用例
W = UserSimilarity_IIF(user_items)
rank = Recommand("Allen", user_items, W, 3)
print rank
测试结果
{'c': 0.6750166837258342, 'e': 0.6750166837258342}
可以看到这两个物品的评分都降低了,因为这个样例的物品太少,所以每个物品都是热门物品。
1.4 UserCF 的缺陷
基于用户的协同过滤存在一些缺陷
(1)随着用户数量增长,相似度矩阵的计算将越来越困难,其运算时间和空间复杂度都是
O(U2)
O
(
U
2
)
。
(2)UserCF的可解释性不够强。
二、基于物品的协同过滤 ItemCF
基于物品的协同过滤会向用户和推荐他之前喜欢的物品相似的物品,它通过分析用户的行为记录,计算物品之间的相似度,而不是简单地利用物品本身的属性来计算。也就是说,若喜欢物品
i
i
的用户大多也喜欢物品,才认为物品
i
i
和具有相似性。
ItemCF的基本步骤有两步:
(1)计算物品之间的相似度。
(2)根据物品的相似度和用户的历史行为给用户生成推荐列表。
2.1 如何衡量物品之间的相似性
物品
j
j
和的相似度定义为喜欢物品
i
i
的用户中,同时也喜欢物品的用户的比例,分母引入
N(j)
N
(
j
)
的原因是为了避免
j
j
是热门物品而产生偏向。
假设每一个用户的兴趣都局限于某几个领域,如果两个物品同属于一个用户的兴趣列表,则表示它们同属于有限的几个(但不同的)领域,但如果两个物品同属于很多用户的兴趣列表,则它们很可能属于同一个领域,因此具有较大相似性。
python实现计算物品相似度
与UserCF的实现类似,ItemCF是先构造用户-物品倒排表,然后计算物品之间的相似度,下面这个函数能实现物品相似度的计算。
def ItemSimilarity(item_users):
# 构造用户-物品倒排表
user_items = {}
for item, user_set in item_users.iteritems():
for k in user_set:
if k not in user_items:
user_items[k] = set()
user_items[k].add(item)
C = {}
N = {}
for u, item_set in user_items.iteritems():
for i in item_set:
N[i] = 0 if i not in N else N[i]
C[i] = {} if i not in C else C[i]
N[i] += 1
for j in item_set:
if i!=j:
C[i][j] = 0 if j not in C[i] else C[i][j]
C[i][j] += 1
# 计算物品相似度
W = {}
for i, related_items in C.iteritems():
W[i] = {} if i not in W else W[i]
for j, cij in related_items.iteritems():
if cij!=0:
W[i][j] = 0 if j not in W[i] else W[i][j]
W[i][j] += cij / np.sqrt(N[i] * N[j])
return W
测试用例
item_users = {'a':{"Allen","Eric"},
'b':{'Allen','Ben','Denny'},
'c':{'Ben','Copper','Denny'},
'd':{'Allen','Copper','Denny',"Eric"},
'e':{'Ben'}}
W = ItemSimilarity(item_users)
# 从稀疏表达转矩阵
result = np.zeros((5,5))
items = ['a','b','c','d','e']
for i, related_items in W.iteritems():
for j, w in related_items.iteritems():
result[items.index(i)][items.index(j)] = w
print result
测试结果
[[0. 0.40824829 0. 0.70710678 0. ]
[0.40824829 0. 0.66666667 0.57735027 0.57735027]
[0. 0.66666667 0. 0.57735027 0.57735027]
[0.70710678 0.57735027 0.57735027 0. 0. ]
[0. 0.57735027 0.57735027 0. 0. ]]
对于这个结果,我们观察一下第一行第四列,即物品a和d的相似度,它是最高的,显而易见,因为物品d的用户列表完全涵盖了物品a的用户列表。
2.2 如何为用户推荐物品
在得到物品之间的相似度之后,ItemCF用下面这个公式计算用户u对于物品
i
i
的兴趣,它的直观意义是,在用户喜欢的物品集合
N(u)
N
(
u
)
里面,找到与物品
i
i
最相似的个,计算他们的加权评分,作为用户
u
u
对物品的兴趣值。
python实现推荐
def Recommand(user, user_items, W, K):
rank = {}
ru = user_items[user] # 用户user已经发生过交互的物品
for i, pi in ru.iteritems():
for j, wj in sorted(W[i].iteritems(), key=lambda x:x[1], reverse=True)[:K]:
if not j in ru: # 只推荐用户没有发生过交互的物品
rank[j] = 0 if j not in rank else rank[j]
rank[j] += pi * wj
return rank
测试用例
user_items = {"Allen": {'a':1, 'b':1, 'd':1},
"Ben": {'b':1, 'c':1, 'e':1},
"Copper":{'c':1, 'd':1},
"Denny": {'b':1, 'c':1, 'd':1},
"Eric": {'a':1, 'd':1}}
rank = Recommand("Allen", user_items, W, K=3)
print rank
测试结果
{'c': 1.2440169358562925, 'e': 0.5773502691896258}
这个结果也是显而易见的,按照兴趣值公式的定义,我们要给用户Allen的推荐他的兴趣列表里没出现的物品,即c和e。Allen对c的兴趣的计算,即在W矩阵寻找c对应的一行里与Allen兴趣列表重合的项(即b和d),由于在这个例子里 r(u,i) r ( u , i ) 总是1,我们只需要把0.6666和0.5774加起来,正是1.244。
带解释的ItemCF算法
给出兴趣值的同时,告诉用户这个兴趣值是从哪些物品计算而来的。
class record():
def __init__(self, w, r):
self.weight = w
self.reason = r
def Recommand_withReason(user, user_items, W, K):
rank = {}
ru = user_items[user]
for i, pi in ru.iteritems():
for j, wj in sorted(W[i].iteritems(), key=lambda x:x[1], reverse=True)[:K]:
if not j in ru:
rank[j] = record(0,{}) if j not in rank else rank[j]
rank[j].weight += pi * wj
rank[j].reason[i] = pi * wj
return rank
测试样例
user_items = {"Allen": {'a':1, 'b':1, 'd':1},
"Ben": {'b':1, 'c':1, 'e':1},
"Copper":{'c':1, 'd':1},
"Denny": {'b':1, 'c':1, 'd':1},
"Eric": {'a':1, 'd':1}}
rank = Recommand_withReason("Allen", user_items, W, K=3)
for recommanded_item, record in rank.iteritems():
print 'recommand', recommanded_item, record.weight
print 'computed from', [(k,v) for k,v in record.reason.iteritems()]
测试结果
recommand c 1.24401693586
computed from [('b', 0.6666666666666666), ('d', 0.5773502691896258)]
recommand e 0.57735026919
computed from [('b', 0.5773502691896258)]
2.3 改进的ItemCF推荐
ItemCF-IUF
物品之间之所以能进行相似度衡量,是因为它们同时出现在多个用户的兴趣列表中,也就是说,每个用户的兴趣列表都将对物品的相似度产生贡献,但是每个用户的贡献是不同的。假设某用户是开书店的,他从京东上买了80万本书,但是他购买这些书并非出于自身的兴趣,因此他对于书与书之间的相似度产生的贡献应该很小,至少应该远远小于一个只买了几本自己最爱的侦探小说的用户。
论文提出了Item-IUF(Inverse User Frequency),他认为活跃的用户对物品相似度的贡献应该小于不活跃的用户。实际上对于一些过于活跃的用户,会直接排除而不参与相似度计算。
python实现 ItemCF-IUF
与普通的ItemCF相比,ItemCF-IUF只需要在计算C矩阵(分子)时略作修改即可。
def ItemSimilarity_IUF(item_users):
# 构造用户-物品倒排表
user_items = {}
for item, user_set in item_users.iteritems():
for k in user_set:
if k not in user_items:
user_items[k] = set()
user_items[k].add(item)
C = {}
N = {}
for u, item_set in user_items.iteritems():
for i in item_set:
N[i] = 0 if i not in N else N[i]
C[i] = {} if i not in C else C[i]
N[i] += 1
for j in item_set:
if i!=j:
C[i][j] = 0 if j not in C[i] else C[i][j]
C[i][j] += 1 / np.log(1 + len(item_set))
# 计算物品相似度
W = {}
for i, related_items in C.iteritems():
W[i] = {} if i not in W else W[i]
for j, cij in related_items.iteritems():
if cij!=0:
W[i][j] = 0 if j not in W[i] else W[i][j]
W[i][j] += cij / np.sqrt(N[i] * N[j])
return W
测试用例
item_users = {'a':{"Allen","Eric"},
'b':{'Allen','Ben','Denny'},
'c':{'Ben','Copper','Denny'},
'd':{'Allen','Copper','Denny',"Eric"},
'e':{'Ben'}}
W = ItemSimilarity_IUF(item_users)
测试结果
[[0. 0.29448889 0. 0.57685303 0. ]
[0.29448889 0. 0.48089835 0.41647019 0.41647019]
[0. 0.48089835 0. 0.47099852 0.41647019]
[0.57685303 0.41647019 0.47099852 0. 0. ]
[0. 0.41647019 0.41647019 0. 0. ]]
不同的用户对物品相似度的贡献都发生了衰减,所以我们可以看到整个W矩阵所有元素都发生了衰减,那些特别活跃的用户衰减得越厉害。
相似度的归一化
论文提到如果将ItemCF计算得到的W矩阵按物品归一化(即按行归一化),可以提高推荐系统的Precision(准确率)。
为什么要做归一化呢?假设物品a和d是同一类(A类),而物品b和c又是同一类(B类),一般来说总有同类物品之间的相似度大于异类物品之间的相似度,但是每类物品的相似度水平是不一样的。在下面这个例子中,A类物品的相似度水平就比B类物品的相似度高。假设某用户的兴趣列表里有10个A类物品和10个B类物品,根据ItemCF兴趣值公式,用户对A类物品的兴趣会比B大,系统给他推荐的就可能都是A类物品了。一般来说,热门物品的类内相似度会比较高,这样系统就会偏向于给用户推荐热门物品,覆盖率就会变低。
按物品归一化之后,A类物品之间的相似度是1,B类物品之间的相似度也是1,这样即使用户兴趣列表里A类和B类物品数量相同,给他推荐的物品也会是由A和B类物品共同组成的集合,而不会偏向于某一方,也就是说,可以提高推荐系统的覆盖率。
# ItemCF计算的W矩阵
[[0. 0.40824829 0. 0.70710678 0. ]
[0.40824829 0. 0.66666667 0.57735027 0.57735027]
[0. 0.66666667 0. 0.57735027 0.57735027]
[0.70710678 0.57735027 0.57735027 0. 0. ]
[0. 0.57735027 0.57735027 0. 0. ]]
# 按行归一化
[[0. 0.57735027 0. 1. 0. ]
[0.61237243 0. 1. 0.8660254 0.8660254 ]
[0. 1. 0. 0.8660254 0.8660254 ]
[1. 0.81649658 0.81649658 0. 0. ]
[0. 1. 1. 0. 0. ]]
2.4 ItemCF的缺陷
(1)覆盖率和流行度都不高。
三、UserCF VS ItemCF
两者的关注点不同。UserCF为用户推荐和他有共同兴趣爱好的用户所喜欢的物品,ItemCF给用户推荐和他以前喜欢的物品相似的物品。所以UserCF着重于反映和目标用户所属的同兴趣小群体的热点,UserCF推荐更加社会化,而ItemCF着重于反映用户的个人爱好,ItemCF推荐更加个性化。UserCF更适合用户个性化兴趣不太明显的场景,ItemCF更适合长尾物品丰富且用户个性需求强烈的场景。
两者的性能偏好不同。UserCF偏向于用户远少于物品,而且物品更新频率很高的场景,比如新闻推荐,物品就是每时每刻都实时更新的新闻,维护Item之间的相似度矩阵的代价远大于维护用户相似度矩阵。ItemCF偏向于物品远少于用户,而且物品更新频率不高的场景,比如各大电商平台(亚马逊、淘宝之类),物品的更新频率不会太快(相似度矩阵一天一更也是可以接受的)。
两者关于用户的实时性不同。UserCF中目标用户有新行为,他所属的兴趣团体可能不会有太大的变化,所以推荐结果不一定会立即变化。ItemCF中目标用户有新行为,一定会导致推荐结果的实时变化。
两者对冷启动的表现不同。UserCF并不能马上为新用户推荐物品,至少需要用户对少量物品产生过行为,才能更新用户相似度。对于上线的新物品,只要有用户对它发生了行为,UserCF就能马上将它推荐给该用户所属的兴趣群体。ItemCF,只要新用户对一个物品产生了行为,就可以立刻给他推荐和该物品相似的其他物品。而新物品上线之后,必须在更新过物品相似度之后才能进行推荐。
两者的可解释性不同。给用户推荐物品时,比如《推荐系统实践》这本书。UserCFA能给出的解释可能是“用户张三(你并不认识)购买了该书”,ItemCF给出的解释则是“您两个月前购买了《推荐系统介绍》”,显然后者更令人信服。
现实中,用户的数目往往非常巨大,远远大于物品数目,这在电商平台表现得尤其突出,维护物品相似度的代价比维护用户相似度的代价要小。同时物品的更新速度一般不会太快,则更新物品相似度的频率也不会太高。所以使用ItemCF一般是更好的选择。当然凡事无绝对,ItemCF也有他的缺陷,比如很容易推荐热门物品,UserCF在特殊场景中也有其不可取替的优势,所以还是得具体问题具体分析。
说到底,这两种协同过滤都属于比较古老的技术(十几二十年前提出来的了),如果对这方面感兴趣,可以去跟进一下这方面的新技术和论文。
参考资料
【论文】Breese J S, Heckerman D, Kadie C. Empirical analysis of predictive algorithms for collaborative filtering[C] // Proc. Conference of Uncertainty in Articial Intelligence. 1998:43-52. 论文链接
【论文】Karypis G. Evaluation of Item-Based Top-N Recommendation Algorithms[C] // Tenth International Conference on Information and Knowledge Management. ACM, 2001:247-254. 论文链接
【书籍】《推荐系统实践》项亮
【书籍】《推荐系统》Dietmar Jannach等