推荐系统的经典算法就是协同过滤了,协同过滤算法有两种,一种是基于物品的,一种是基于用户的。从很多实验效果来看基于用户的协同过滤算法要好于基于物品的协同过滤算法。
那么简单来说基于物品的协同过滤算法是说我会推荐给你和你喜欢物品相似的物品,而基于用户的协同过滤算法是说我把和你相似的用户喜欢的东西推荐给你。为什么叫协同过滤呢,因为我们是利用用户的群体行为来作这些相似操作的。计算物品的相似的时候我们比较不同的人来对他打分来比较,同样计算用户相关性的时候我们就是通过对比他们对相同物品打分的相关度来计算的,我们来举个例子。
--------+--------+--------+--------+--------+
| X | Y | Z | R |
--------+--------+--------+--------+--------+
a | 5 | 4 | 1 | 5 |
--------+--------+--------+--------+--------+
b | 4 | 3 | 1 | ? |
--------+--------+--------+--------+--------+
c | 2 | 2 | 5 | 1 |
--------+--------+--------+--------+--------+
a用户给X物品打了5分,Y打了4分,Z打了1分,同理b用户和c用户,那么很容易看到a用户和b用户非常相似,但是b用户没有看过R物品,那么我们就可以把和b用户很相似的a用户打分很高的R物品推荐给b用户,这就是基于用户的协同过滤。
ok,回到我们协同过滤的算法上,现在我们知道了基于用户的协同过滤需要比较用户的相关性,那么如何计算这个相关性呢,于是我们可以利用两个用户对于相同物品的评分来计算相关性。对于a,b用户而言,他们都对XYZ物品进行了评价,那么,a我们可以表示为(5,4,1),b可以表示为(4,3,1),经典的算法是计算把他们看作是两个向量,并计算两个向量间的夹角,或者说计算向量夹角的cosine值来比较,于是a和b的相关性为。
这个值介于-1到1之间,越大,说明相关性越大。
到这里似乎cosine还是不错的,但是考虑这么个问题,用于用户间的差异,d用户可能喜欢打高分,e用户喜欢打低分,f用户喜欢乱打分。
--------+--------+--------+--------+
| X | Y | Z |
--------+--------+--------+--------+
d | 4 | 4 | 5 |
--------+--------+--------+--------+
e | 1 | 1 | 2 |
--------+--------+--------+--------+
f | 4 | 1 | 5 |
--------+--------+--------+--------+
很显然用户d和e对于作品评价的趋势是一样的,所以应该认为d和e更相似,但是用cosine计算出来的只能是d和f更相似。于是就有皮尔逊相关系数(pearson correlation coefficient)。
pearson其实做的事情就是先把两个向量都减去他们的平均值,然后再计算cosine值。
最后让我们用实际数据来对比下cosine和pearson的效果吧。这里我们用到了movielens的数据,数据是1000多个用户对于1700个movie的超过10000的评分数据,数据已经分成多组,并且每组都是80%的训练数据和20%的测试数据。我们在训练数据上对于每个用户找出和他相似的20个用户,然后把当前用户没看过的这些用户的movie的评分加权和,然后选出5篇分数最高的作为推荐,然后把推荐出来的在测试数据上计算一个得分。代码如下。
# -*- coding: utf-8 -*-
import heapq
name = 'u1'
def get(f):
ret = {}
for i in open(f, 'r'):
tmp = map(int, filter(lambda x:len(x)>0, i.split('\t')))
if tmp[0] not in ret:
ret[tmp[0]] = {}
ret[tmp[0]][tmp[1]] = tmp[2]
return ret
def cosine(item1, item2):
sum0 = sum(map(lambda x:x[0]*x[1], zip(item1, item2)))
sum1 = sum(map(lambda x:x*x, item1))
sum2 = sum(map(lambda x:x*x, item2))
return sum0/(sum1**0.5)/(sum2**0.5)
def pearson(item1, item2):
a1 = (sum(item1)+0.0)/len(item1)
a2 = (sum(item2)+0.0)/len(item2)
sum0 = sum(map(lambda x:(x[0]-a1)*(x[1]-a2), zip(item1, item2)))
sum1 = sum(map(lambda x:(x-a1)*(x-a1), item1))
sum2 = sum(map(lambda x:(x-a2)*(x-a2), item2))
if not sum1 or not sum2:
return cosine(item1, item2)
return sum0/(sum1**0.5)/(sum2**0.5)
def get_sim(user):
ret = {}
for i in user:
ret[i] = {}
for j in user:
itemset = set(user[i].keys())&set(user[j].keys())
tmp1 = map(lambda x:x[1], filter(lambda y:y[0] in itemset, sorted(user[i].items())))
tmp2 = map(lambda x:x[1], filter(lambda y:y[0] in itemset, sorted(user[j].items())))
if not len(tmp1):
ret[i][j] = 0
else:
ret[i][j] = cosine(tmp1, tmp2)
return ret
def get_re(user, sim):
ret = {}
for i in user:
tmp = filter(lambda y:y[0]!=i, heapq.nlargest(20, sim[i].items(), key=lambda x:x[1]))
tmp_res = {}
for j in tmp:
for k in user[j[0]]:
if k in user[i]:
continue
if k not in tmp_res:
tmp_res[k] = 0
tmp_res[k] += j[1]*user[j[0]][k]
ret[i] = map(lambda x:x[0], heapq.nlargest(5, tmp_res.items(), key=lambda x:x[1]))
return ret
def test_score(test_data, re):
score = 0
for i in test_data:
u = test_data[i]
r = re[i]
tmp = 0
for i in r:
if i in u:
tmp += u[i]
score += (tmp+0.0)/len(r)
return score/len(test_data)
def main():
data1 = get(name+'.base')
data2 = get(name+'.test')
sim = get_sim(data1)
re = get_re(data1, sim)
print test_score(data2, re)
if __name__ == '__main__':
main()
最后我们看看结果。
从图中可以看出,用pearson来计算用户相似来进行推荐的话,效果还是要好于cosine的。所以说基于用户的协同过滤还是用pearson来做用户相似是比较好的阿。