协同过滤,之所以协同是因为该方法是基于其他用户进行推荐的。工作流程如下:假设我们的任务是向你推荐一本书。我们会在网络上搜索与你相似兴趣的用户。一旦找到了这个用户,就看看这个用户喜欢的书,然后将其推荐给你。
如何寻找相似用户?一般而言,通过计算两个用户的距离,两个用户距离越小则相似度越高。先来看距离的定义。
1 曼哈顿距离(Manhattan Distance)N维下,表示为:d(x,y)=sigma(|xk-yk|) (k=1...n)
2 欧氏距离 (Euclidean Distance) N维下,表示为:d(x,y)=(sigma((xk-yk)2))1/2 (k=1,2...n)
3 明氏距离 (Minkoeski Distance) N维下,表示为:d(x,y)=(sigma(|xk-yk|r))1/r ,可见,当r=1时,即为曼哈顿距离,r=2时为欧氏距离,r=∞时,为上确界距离(Supermun Distance)
以下是8个用户对8个音乐的评分。通过计算两个人的明氏距离来实现推荐
#!/usr/bin/env python # -*- coding: utf-8 -*- # __author__ : '小糖果' import xlrd import json def load_datas(filepath): wb = xlrd.open_workbook(filepath) table = wb.sheet_by_name('Sheet1') users = {} # 用户字典 for c in range(1,table.ncols): # 循环每一列 name = table.cell_value(0,c) # 用户名 users[name] = {} # 建立空字典 for r in range(1,table.nrows): # 循环每一列 if table.cell_value(r,c): # 如果当前值不空 key = table.cell_value(r,0) value = float(table.cell_value(r,c)) #建立字典 users[name][key] = value return users def minkowski(rating1,rating2,r): """ Compute the minkowski diatance between tow uses """ distance = 0.0 for key in rating1: if key in rating2: distance += pow((abs(rating1[key]-rating2[key])),r) return pow(distance,1./r) def find_nearest_neighber(username,users): """ Create a sorted list bsaed on their distance """ neighbers = [] for user in users: if username != user: d = minkowski(users[username],users[user],2) neighbers.append((d,user)) # sort by distance neighbers.sort(cmp) return neighbers # 返回最近的人 def recommend(username,users): """ Give list of recomends """ recommends = [] neighber = find_nearest_neighber(username,users)[0][1] for artist in users[neighber]: if artist not in users[username]: recommends.append((artist,users[neighber][artist])) return sorted(recommends,key = lambda artist_tupe:artist_tupe[1],reverse = True) def test(): filepath = r'C:UsersTDDesktopPythonProject ecords.xlsx' users = load_datas(filepath) print recommend('Chan',users) print recommend('Hailey',users) if __name__ == '__main__': test()
用户评级差异
对用户评级结果进行考察发现,单个用户的评级行为差距很大。上表中,Bill想避免极端结果,它的评级都在2到4之间,而Jordy好像喜欢任何乐队,他的评级都在4到5.而Hailey只有两种选择,不是1就是4.解决这个问题的方法是考虑皮尔逊相关系数(Pearson Correlation Coefficient) 。皮尔逊相关系数是度量两个变量的相关性指标,取值区间为[-1,1]
值越接近1表示相关性越好。皮尔逊计算公式如下:
但是一般而言,上述公式计算复杂度达到O(n2)。实际应用时采用以下近似公式:
有这样一个列子,在音乐播放器上有1.5亿首歌曲,现在来考虑歌曲播放的次数问题。对于个人而言,播放的歌曲数有限,可能就4000首,也就是说其他歌曲播放次数为0,于是对于个人来说,得到的数据信息是很稀少的。而在1.5亿首个上去比较2个人时,绝大部分公共为0.这种情况下,无论使用明氏距离还是皮尔逊相关系数都是不明智的。我们还有一种选择,那就是余弦相似度,公式如下:
也就是欧氏空间中两个N维向量的夹角的余弦。余弦相似度取值在1到-1之间,1表示完全相似,-1表示完全不相似。
那么问题来了。上面说了三个相似度,在时间问题中如何选择呢?可以按照以下规则:
1 如果数据稠密(几乎所有的属性搜没有0值),那么使用明氏距离比较合适
2 如果数据受到分数贬值(即为不同用户使用不同的评价范围),则使用皮尔逊相关系数比较合理
3 如果数据稀疏(很多0出现),那么考虑使用余弦相似度是合理的
一些怪异的事情:
当我们在依赖最相似的用户进行推荐时,该用户的个人怪癖也会被推荐,而这些怪癖别是可能是不喜欢的。一种解决的办法是基于多个相似用户进行推荐。这里采用k近邻法。在协同过滤的k近邻法中,我们用k个最相似的用户来确定推荐结果。每一个用户根据相似度大小获得一个权重。
#!/usr/bin/env python # -*- coding: utf-8 -*- # __author__ : '小糖果' import json import sys from math import sqrt from pprint import pprint class Recommender(object): def __init__(self, data,k = 1,n = 5): """ initialize recomender,data is dict k is the value for k nearest neighbor n is the maximum number recommendations to make """ self.k = k self.n = n if type(data).__name__ == 'dict': self.data = data else: raise Exception('输入数据类型必须是字典') sys.exit(1) def pearson(self,rating1, rating2): sum_xy = 0 sum_x = 0 sum_y = 0 sum_x2 = 0 sum_y2 = 0 n = 0 for key in rating1: if key in rating2: n += 1 x = rating1[key] y = rating2[key] sum_xy += x * y sum_x += x sum_y += y sum_x2 += x ** 2 sum_y2 += y ** 2 if n == 0: return 0 denominator = sqrt(sum_x2 - (sum_x ** 2) / n) * sqrt(sum_y2 - (sum_y ** 2) / n) if denominator == 0: return 0 else: return (sum_xy - sum_x * sum_y / n) / denominator def find_nearest_neighbors(self, username): neighbors = [] for user in self.data: if username != user: per = self.pearson(self.data[user], self.data[username]) neighbors.append((user,per)) return sorted(neighbors,key = lambda element:element[1],reverse = True) def recommend(self,username): neighbors = self.find_nearest_neighbors(username) tol_distance = 0 for i in range(self.k): tol_distance += neighbors[i][1] recommends = {} user_ratings = self.data[username] for i in range(self.k): name = neighbors[i][0] neighbor_ratings = self.data[name] weight = neighbors[i][1]/tol_distance for artist in neighbor_ratings: if artist not in user_ratings: try: recommends[artist] += weight*neighbor_ratings[artist] except KeyError: recommends[artist] = weight*neighbor_ratings[artist] recommends = [(item[0],item[1]) for item in recommends.items()] recommends.sort(key = lambda element:element[1],reverse = True) return recommends[:self.n] def test(): filepath = r'C:UsersTDDesktopPythonProject ecords.json' with open(filepath,'rb') as f: data = json.load(f) instance = Recommender(data) print instance.recommend('Hailey') if __name__ == '__main__': test()