推荐算法入门-python
文章内容:基于物品过滤与基于用户过滤。
数据稀疏时候,用物品过滤最优;数据密集,两者效果一样。
下面以电影推荐为例:
一、原始数据处理:
原始数据为二维矩阵:行是用户,列是电影:
Lady in the Water | Snakes on a Plane | Just My Luck | Superman Returns | You, Me and Dupree | The Night Listener | |
Lisa Rose | 2.5 | 3.5 | 3.5 | 3.5 | 2.5 | 3 |
Gene Seymour | 3 | 3.5 | 1.5 | 5 | 3.5 | 3 |
Michael Phillips | 2.5 | 3 | 3.5 | 4 | ||
Claudia Puig | 3.5 | 3 | 4 | 2.5 | 4.5 | |
Mick LaSalle | 3 | 4 | 2 | 3 | 2 | 3 |
Jack Matthews | 3 | 4 | 5 | 3.5 | 3 | |
Toby | 4.5 | 4 | 1 |
采用数据结构:字典套字典:如此即可表现出数据二维矩阵:结构即data[user][movie]=rating
此数据整体是一个字典,key=用户,value=该用户看过的电影信息 ,其中value又是一个字典(key=电影,value=评分)
data ={'Lisa Rose': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.5, 'Just My Luck': 3.0, 'Superman Returns': 3.5, 'You, Me and Dupree': 2.5, 'The Night Listener': 3.0}, 'Gene Seymour': {'Lady in the Water': 3.0, 'Snakes on a Plane': 3.5, 'Just My Luck': 1.5, 'Superman Returns': 5.0, 'The Night Listener': 3.0, 'You, Me and Dupree': 3.5}, 'Michael Phillips': {'Lady in the Water': 2.5, 'Snakes on a Plane': 3.0, 'Superman Returns': 3.5, 'The Night Listener': 4.0}, 'Claudia Puig': {'Snakes on a Plane': 3.5, 'Just My Luck': 3.0, 'The Night Listener': 4.5, 'Superman Returns': 4.0, 'You, Me and Dupree': 2.5}, 'Mick LaSalle': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0, 'Just My Luck': 2.0, 'Superman Returns': 3.0, 'The Night Listener': 3.0, 'You, Me and Dupree': 2.0}, 'Jack Matthews': {'Lady in the Water': 3.0, 'Snakes on a Plane': 4.0, 'The Night Listener': 3.0, 'Superman Returns': 5.0, 'You, Me and Dupree': 3.5}, 'Toby': {'Snakes on a Plane':4.5,'You, Me and Dupree':1.0,'Superman Returns':4.0} }
二、物品过滤
1.先计算所有电影(物品)之间的相似度,构造一个包含相近电影(物品)的完整数据集。
1.1 匹配电影物品,找出那些电影物品是彼此相近的
因为计算以电影物品为主,所以先将上面data[user][movie]数据转换成newdata[movie][user]格式,也即二维矩阵进行行列对换
def transformdata(data): ''' 物品之间的相似度 与 用户之间的相似度 求解 一样。故只需要将用户换成物品即可 ''' newdata = {} users ={} for person in data: for movie in data[person]: #初始化 newdata.setdefault(movie,{}) #物品与用户对调 newdata[movie][person] = data [person][movie] #字典可以直接写[key],就表示插入key值了。非常简便 return newdata
''' 调用此方法例子: print transformdata(data) 结果是: {'Lady in the Water': {'Lisa Rose': 2.5, 'Jack Matthews': 3.0, 'Michael Phillips': 2.5, 'Gene Seymour': 3.0, 'Mick LaSalle': 3.0}, 'Snakes on a Plane': {'Jack Matthews': 4.0, 'Mick LaSalle': 4.0, 'Claudia Puig': 3.5, 'Lisa Rose': 3.5, 'Toby': 4.5, 'Gene Seymour': 3.5, 'Michael Phillips': 3.0}, 'Just My Luck': {'Claudia Puig': 3.0, 'Lisa Rose': 3.0, 'Gene Seymour': 1.5, 'Mick LaSalle': 2.0}, 'Superman Returns': {'Jack Matthews': 5.0, 'Mick LaSalle': 3.0, 'Claudia Puig': 4.0, 'Lisa Rose': 3.5, 'Toby': 4.0, 'Gene Seymour': 5.0, 'Michael Phillips': 3.5}, 'The Night Listener': {'Jack Matthews': 3.0, 'Mick LaSalle': 3.0, 'Claudia Puig': 4.5, 'Lisa Rose': 3.0, 'Gene Seymour': 3.0, 'Michael Phillips': 4.0}, 'You, Me and Dupree': {'Jack Matthews': 3.5, 'Mick LaSalle': 2.0, 'Claudia Puig': 2.5, 'Lisa Rose': 2.5, 'Toby': 1.0, 'Gene Seymour': 3.5}} '''
1.2 用calSimilarItems()获得所有电影之间的相似度。
1.2.1 计算相似度,就要涉及相似距离度量,这里列举两种:欧氏距离sim_distance ,皮尔逊sim_pearson 。两种都已设法表示距离越大,越相似
from math import sqrt def sim_distance(data,person1,person2): '''欧氏距离求相似度,距离越大,越相似''' commonmovies = [ movie for movie in data[person1] if movie in data[person2]] if len(commonmovies)== 0: return 0 #平方和 sumSq =sum([pow(data[person1][movie] -data[person2][movie],2) for movie in commonmovies ] ) #使最终结果是,越相似,距离越大。所以将上面距离取倒数即可 sim = 1/(1+ sqrt(sumSq)) return sim def sim_pearson(data,person1,person2): ''' 计算上面格式的数据 里的 两个用户 相似度. 基于用户过滤思路:找出两个用户看过的相同电影的评分,从而进行按pearson公式求值。那些非公共电影不列入求相似度值范围。
基于物品过滤思路:找过两部电影相同的观影人给出的评分,从而按pearson公式求值 返回:评分的相似度,[-1,1]范围,0最不相关,1,-1为正负相关,等于1时,表示两个用户完全一致评分 这里的data格式很重要,这里计算相似度是严格按照上面data格式所算。 此字典套字典格式,跟博客计算单词个数 存储格式一样 ''' #计算pearson系数,先要收集两个用户公共电影名单 #commonmovies = [ movie for movie in data[person1] if movie in data[person2]] 分解步骤为如下: commonmovies = [] #改成列表呢 for movie in data[person1]: #data[person1]是字典,默认第一个元素 in (字典)是指 key.所以这句话是指 对data[person1]字典里遍历每一个key=movie if movie in data[person2]: #data[person2]也是字典,表示该字典有key是movie. commonmovies.append(movie) # commonmovie是 两个用户的公共电影名的列表 #看过的公共电影个数 n = float(len(commonmovies)) if n==0: return 0 '''下面正是计算pearson系数公式 ''' #分布对两个用户的公共电影movie分数总和 sum1 = sum([data[person1][movie]for movie in commonmovies ]) sum2 = sum([data[person2][movie]for movie in commonmovies]) #计算乘积之和 sum12 = sum([data[person1][movie]*data[person2][movie] for movie in commonmovies]) #计算平方和 sum1Sq = sum([ pow(data[person1][movie],2 ) for movie in commonmovies ]) sum2Sq = sum([ pow(data[person2][movie],2 ) for movie in commonmovies ]) #计算分子 num = sum12 - sum1*sum2/n #分母 den = sqrt((sum1Sq - pow(sum1,2)/n)*(sum2Sq - pow(sum2,2)/n)) if den==0: return 0 return num/den
1.2.2 为单个电影物品返回最匹配结果
def topmatches(data,givenperson ,returnernum = 5,simscore = sim_pearson): ''' 用户匹配推荐:给定一个用户,返回对他口味最匹配的其他用户 物品匹配: 给定一个物品,返回相近物品 输入参数:对person进行默认推荐num=5个用户(基于用户过滤),或是返回5部电影物品(基于物品过滤),相似度计算用pearson计算 ''' #建立最终结果列表 usersscores =[(simscore(data,givenperson,other),other) for other in data if other != givenperson ] #对列表排序 usersscores.sort(cmp=None, key=None, reverse=True) return usersscores[0:returnernum]
''' 调用以前方法:找物品相关匹配: moviedata = transformdata(data) #找出跟“超人回归”这电影相关的电影 print topmatches(moviedata, 'Superman Returns') 结果是: [(0.6579516949597695, 'You, Me and Dupree'), (0.4879500364742689, 'Lady in the Water'), (0.11180339887498941, 'Snakes on a Plane'), (-0.1798471947990544, 'The Night Listener'), (-0.42289003161103106, 'Just My Luck')] 其中负数表示,讨厌此电影 '''
1.2.3 基于上面1.2.2,从为单一物品返回匹配结果 扩展到 为所有物品返回匹配结果
def calSimilarItems(data,num=10):
#以物品为中心,对偏好矩阵转置
moviedata = transformdata(data) ItemAllMatches = {} for movie in moviedata: ItemAllMatches.setdefault(movie,[]) #对每个电影 都求它的匹配电影集,求电影之间的距离用欧式距离,用pearson距离测出的结果是不一样的 ItemAllMatches[movie] = topmatches(moviedata, movie, num,simscore = sim_distance) return ItemAllMatches
''' 构造一个物品匹配完整集:包含所有物品的对应的匹配物品 即是电影相似度的字典: key = movie ,value = [(othermovie,simscore)]。其中value是一个元组对的列表 调用例子; print calSimilarItems(data) 结果为: {'Lady in the Water': [(0.7637626158259785, 'Snakes on a Plane'), (0.4879500364742689, 'Superman Returns'), (0.3333333333333333, 'You, Me and Dupree'), (-0.6123724356957927, 'The Night Listener'), (-0.9449111825230676, 'Just My Luck')], 'Snakes on a Plane': [(0.7637626158259785, 'Lady in the Water'), (0.11180339887498941, 'Superman Returns'), (-0.3333333333333333, 'Just My Luck'), (-0.5663521139548527, 'The Night Listener'), (-0.6454972243679047, 'You, Me and Dupree')], 'Just My Luck': [(0.5555555555555556, 'The Night Listener'), (-0.3333333333333333, 'Snakes on a Plane'), (-0.42289003161103106, 'Superman Returns'), (-0.4856618642571827, 'You, Me and Dupree'), (-0.9449111825230676, 'Lady in the Water')], ......... '''
2.推荐用户没看过的电影
某一部未看过电影分数= sum(该部未看过的电影与每一部已看电影之间相似度*已看电影的评分) /sum(未看电影与每一部已看电影之间相似度)
例如:未看电影A,已看电影B,C:
则,电影A分数 = [sim(A,B)*rating(B) +sim(A,C)*rating(C)] / [ sim(A,B) + sim(A,C)]
def getrecommendations(data,targetperson,moviesAllsimilarity): ''' 输入movieAllSimilarity就是上面calsimilarItems已经计算好的所有物品之间的相似度数据集: ''' #获得所有物品之间的相似数据集 scoresum = {} simsum = {} #遍历所有看过的电影 for watchedmovie in data[targetperson]: rating = data[targetperson][watchedmovie] #遍历与当前电影相近的电影 for(similarity,newmovie) in moviesAllsimilarity[watchedmovie]: #取一对元组 #已经对当前物品评价过,则忽略 if newmovie in data[targetperson] :continue
scoresum.setdefault(newmovie,0) simsum.setdefault(newmovie,0) #全部相似度求和 simsum[newmovie] += similarity #评价值与相似度加权之和 scoresum[newmovie] += rating * similarity rankings = [(score/simsum[newmovie] , newmovie) for newmovie,score in scoresum.items() ] rankings.sort(cmp=None, key=None, reverse=True) return rankings
'''调用此方法如下:
itemsAllsim = calSimilarItems(data) #这个值会事先计算好 print '基于物品过滤,为用户Toby推荐的电影是:' print getrecommendations(data, 'Toby',itemsAllsim) 返回结果是: [(3.1667425234070894, 'The Night Listener'), (2.936629402844435, 'Just My Luck'), (2.868767392626467, 'Lady in the Water')]
这样基于物品过滤推荐,为Toby用户推荐的电影结果 就完成了。
'''
三、基于用户过滤
3.1 推荐品味相近的用户
#假设为用户Toby进行推荐品味相当的用户,则调用userstopmatcher方法,会返回一个影评人及其相似度 的列表 print topmatches(data, 'Toby', 3) #返回结果为: [(0.9912407071619299, 'Lisa Rose'), (0.9244734516419049, 'Mick LaSalle'), (0.8934051474415647, 'Claudia Puig')] #所以lisa分数最高,应该推荐lisa Rose品味跟Toby最近
3.2 推荐未看过的电影:
未看过电影分数=sum(被推荐用户与其他用户之间相似度*用户对该电影评分)/sum(被推荐用户与其他用户之间相似度)
def recommendItems(data,givenperson,num =5 ,simscore = sim_pearson): ''' 物品推荐:给定一个用户person,默认返回num=5物品 要两个for,对用户,物品 都进行 遍历 ''' #所有变量尽量用字典,凡是列表能表示的字典都能表示,那何不用字典 itemsimsum={} #存给定用户没看过的电影的其他用户评分加权 itemsum={}
#遍历每个用户,然后遍历该用户每个电影 for otheruser in data : #不要和自己比较 if otheruser == givenperson: continue #忽略相似度=0或小于0情况 sim = simscore(data,givenperson,otheruser) if sim <=0: continue for itemmovie in data[otheruser]: #只对用户没看过的电影进行推荐,参考了其他用户的评价值(协同物品过滤是参考了历史物品相似度值) if itemmovie not in data[givenperson]: #一定要初始化字典:初始化itemsum与itemsimsum itemsum.setdefault(itemmovie,0) itemsimsum.setdefault(itemmovie,0) #用户相似度*评价值 itemsum[itemmovie] += sim * data[otheruser][itemmovie] itemsimsum[itemmovie] += sim #最终结果列表,列表包含一元组(item,分数) rankings = [(itemsum[itemmovie] / itemsimsum[itemmovie],itemmovie) for itemmovie in itemsum] #结果排序 rankings.sort(cmp=None, key=None, reverse=True); return rankings #调用此方法如下:
# print recommendItems(data, 'Toby', 3) # 返回结果:
#[(3.3477895267131013, 'The Night Listener'), (2.8325499182641614, 'Lady in the Water'), (2.5309807037655645, 'Just My Luck')]
四、用真实数据:Movielens数据集
4.1数据原型:
u.item 文件: 记录了 电影id与电影名的映射关系 其内容节选: 1|Toy Story (1995)|01-Jan-1995||http://us.imdb.com/M/title-exact?Toy%20Story%20(1995)|0|0|0|1|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0 2|GoldenEye (1995)|01-Jan-1995||http://us.imdb.com/M/title-exact?GoldenEye%20(1995)|0|1|1|0|0|0|0|0|0|0|0|0|0|0|0|0|1|0|0 u.data 文件: 每行信息是:用户id,电影id,评分,评分时间(这里我们只用到前三个属性) 其内容节选: 196 242 3 881250949 186 302 3 891717742 22 377 1 878887116 244 51 2 880606923
4.2 将movielens数据转换成上面data数据同样格式:
def formatMovieLens(filepath='D:/eclipse 3.62/workspace/Commendations/data/ml-100k'): ''' 这里用的是ml100k数据,只需要里面u.item和u.data两个文件 把movielens数据转换成此代码要求的数据格式,即转换成如同上面data字典套字典结构:dic[user] = {movie:score} ''' movie = {} #获取影片标题。u.item记录着影片id与影片名映射信息 for line in file(filepath+'/u.item'): (id,title) = line.split("|")[0:2] movie[id] = title resultdata = {} for line in file(filepath +'/u.data'): (user,movieid,rating) = line.split("\t")[0:3] resultdata.setdefault(user,{}) movietitle = movie[movieid] resultdata[user][movietitle] = float(rating) return resultdata
4.3 测试结果,为用户进行物品推荐。分别尝试物品过滤,用户过滤
mldata= formatMovieLens() # print "格式化movielens数据集后,取用户id=87看过的电影:" print mldata['87'] #结果是:{'Birdcage, The (1996)': 4.0, 'E.T. the Extra-Terrestrial (1982)': 3.0, 'Bananas (1971)': 5.0, 'Sting, The (1973)': 5.0, 'Bad Boys (1995)': 4.0, 'In the Line of Fire (1993)': 5.0, 'Star Trek: The Wrath of Khan (1982)': 5.0, 'Speechless (1994)': 4.0, .....} #基于用户过滤推荐 print "基于用户过滤推荐:为用户87推荐的电影排名前4部是:" print getrecommendation(mldata, '87' )[0:30] #结果是: [(5.0, 'They Made Me a Criminal (1939)'), (5.0, 'Star Kid (1997)'), (5.0, 'Santa with Muscles (1996)'), (5.0, 'Saint of Fort Washington, The (1993)'), (5.0, 'Marlene Dietrich: Shadow and Light (1996) '), (5.0, 'Great Day in Harlem, A (1994)'), (5.0, 'Entertaining Angels: The Dorothy Day Story (1996)'), (5.0, 'Boys, Les (1997)'), (4.89884443128923, 'Legal Deceit (1997)'), ...] #基于物品过滤推荐 #计算所有物品之间的相似度 itemsim = calSimilarItems(mldata, 50) print "基于物品过滤推荐:为用户87推荐的电影排名前4部是:" print getrecommendedItems(mldata, '87', itemsim)[0:10] # 结果是:[(5.0, "What's Eating Gilbert Grape (1993)"), (5.0, 'Vertigo (1958)'), (5.0, 'Usual Suspects, The (1995)'), (5.0, 'Toy Story (1995)'), (5.0, 'Titanic (1997)'), (5.0, 'Sword in the Stone, The (1963)'), (5.0, 'Stand by Me (1986)'), (5.0, 'Sling Blade (1996)'), (5.0, 'Silence of the Lambs, The (1991)'), (5.0, 'Shining, The (1980)')]