本章主要内容是教会大家【如何根据群体偏好来给人们提供推荐】,这些应用多如牛毛,我就不跟大家像书里那样废话一堆了(不好意思,作者大人),直接进入正题吧,嘻嘻!
协作型过滤
一个协作型过滤算法通常的做法就是对一大群人进行搜索,并从中找出与我们品味相近的一小群人。算法会对这些人所偏爱的其他内容进行考察,并将它们组合起来构造出一个经过排名的推荐列表。
搜集偏好
我们要做的第一件事就是寻找一种表达不同人及其偏好的方法。在python中,达到这一目的的一种非常简单的方法是使用一个嵌套的字典。
# 这是一个字典结构表示每个影评人对所看电影的评分情况,后续所有
#计算均基于该数据
critics={'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(避免遇到被0整除的错误),并取其倒数。如此我们就可以出一个计算相似度的函数:
# 返回一个有关person1和person2的基于距离的相似度评价
def sim_distance(prefs,person1,person2):
# Get the list of shared_items
si={}
for item in prefs[person1]:
if item in prefs[person2]: si[item]=1
# if they have no ratings in common, return 0
if len(si)==0: return 0
# Add up the squares of all the differences
sum_of_squares=sum([pow(prefs[person1][item]-prefs[person2][item],2)
for item in prefs[person1] if item in prefs[person2]])
return 1/(1+sum_of_squares)
在来看看皮尔逊相关度评价,该相关系数是判断两组数据与某一直线的拟合程度的一种度量。它更适用于数据不是很规范(比如评价总是相对于平均水平偏离很大时),会倾向于给出更好的结果,下图展示的是两位影评人(分别为x与y)对不同电影的打分:
图上我们看到了一条直线,因其绘制原则是尽可能地靠近图上的所有坐标点,故而称作“最佳拟合线(best-fit line)”。如果两位评论者对所有影片的评分情况相同,那么这直线将会成为对角线,并且会与图上所有的坐标点相交,从而得到一个结果为1的理想相关度评价。
另外值得注意的是采用皮尔逊方法进行评价时,它修正了“夸大分值(grade inflation)”的情况,比如一个人总是比另一个人给出一个更好的评分,而正好这个分值之差又始终保持一致,则他们依然存在很好的关联性。然后用“欧几里德距离”评价的话,会因为一个人总比另一个人的评分要高,导致两者不相近的结论,即使他们品味相似。而这一行为是否是我们需要的结果,则取决于具体的应用场景。
皮尔逊相关度评价算法首先会找出两位评论者都曾评价过的物品,然后计算两者的评分总和与平方和,并求得评分的乘积之和。最后,算法利用这些结果计算出皮尔逊相关系数,这一公式相对于欧几里德不是非常直观,但是通过除以将所有变量的变化值相乘后得到得结果,它的确能告诉我们变量的总体变化情况。
实现算法如下:
# 返回p1和p2的皮尔逊相关系数
def sim_pearson(prefs,p1,p2):
# Get the list of mutually rated items
si={}
for item in prefs[p1]:
if item in prefs[p2]: si[item]=1
# if they are no ratings in common, return 0
if len(si)==0: return 0
# Sum calculations
n=len(si)
# Sums of all the preferences
sum1=sum([prefs[p1][it] for it in si])
sum2=sum([prefs[p2][it] for it in si])
# Sums of the squares
sum1Sq=sum([pow(prefs[p1][it],2) for it in si])
sum2Sq=sum([pow(prefs[p2][it],2) for it in si])
# Sum of the products
pSum=sum([prefs[p1][it]*prefs[p2][it] for it in si])
# Calculate r (Pearson score)
num=pSum-(sum1*sum2/n)
den=sqrt((sum1Sq-pow(sum1,2)/n)*(sum2Sq-pow(sum2,2)/n))
if den==0: return 0
r=num/den
return r
为评论者打分,找到自己人!
接下来我们在实际场景下使用上面的算法,我们根据指定人员对每个人进行打分,并找出最接近的匹配结果,本例我们就是找寻与自己有相似品味的影评人,因为这样我们就知道在选择影片时我们应该采纳谁的建议了,太棒了!下面就是实现代码:
# Returns the best matches for person from the prefs dictionary.
# Number of results and similarity function are optional params.
def topMatches(prefs,person,n=5,similarity=sim_pearson):
scores=[(similarity(prefs,person,other),other)
for other in prefs if other!=person]
scores.sort()
scores.reverse()
return scores[0:n]
该函数利用了Python的列表推导式,采用先前定义过的皮尔逊相关度评价算法,将自身与字典中的其他每一个用户进行比较,然后函数返回排序结果的前n项
推荐物品
通过上文能找到趣味相投的影评者固然不错,但跟时常的情况是我们需要的是一份影片的推荐,通过在与自己品味相近的人那里挑选他所喜欢的影片一个是随意,一个是累,而且有时候影评人自己都还没看过新的电影,当然也不存在相应的评论。 更可怕的情况时,我们可能找到一个热衷某一部影片的古怪评论者,而根据topMatches所返回的结果,其他评论者都不看好该影片。
为了解决该问题,我们需要通过一个经过加权的评价值来为影片打分,评论者的评分结果因此而形成了先后的排名。为此我们需要取得所有评论者的评价结果,借此得到相似度后,再乘以他们为每部影片所给的评价值。如下表:
如此,相比于与我们不相似的人,那些与我们相似的人将会对整体评价值拥有更多的贡献。我们也可以选择利用总值来计算排名,但是我们必须要要考虑到更多的影评人可能会对结果造成更大的影响。为了修正这一问题,我们通过除以相似度之和(Sim.Sum)。
下列代码反映了上述过程,非常简单易懂,并且它对欧几里德距离评价或者皮尔逊相关度评价都是适用的:
# Gets recommendations for a person by using a weighted average
# of every other user's rankings
# 利用所有他人评价值的加权平均,为默认提供建议
def getRecommendations(prefs, person, similarity=sim_pearson):
totals = {}
simSums = {}
for other in prefs:
# don't compare me to myself
#不要和自己比较
if other == person: continue
sim=similarity(prefs,person,other)
# ignore scores of zero or lower
# 忽略评价为0或者小于0的情况
if sim <= 0: continue
for item in prefs[other]:
# only score movies I haven't seen yet
# 只对自己还未看过的电影进行评价
if item not in prefs[person] or prefs[person][item]==0:
# Similarity * Score
totals.setdefault(item,0)
totals[item] += prefs[other][item]*sim
# Sum of similarities
simSums.setdefault(item,0)
simSums[item] += sim
# Create the normalized list
# 建立一个归一化列表
rankings = [(total/simSums[item], item) for item, total in totals.items()]
# Return the sorted list
rankings.sort()
rankings.reverse()
return rankings
匹配商品
我们已经知道如何为指定人员寻找品味相近者,以及如何推荐商品,但是假如我们想了解哪些商品是彼此相近的,那该如何做呢?在这种情况下,我们可以通过产看哪些人喜欢某一个特定物品,以及这些人喜欢其他物品来决定相似度,事实上和我们上面在获取人与人的相似度的方法一样——只需要在人员和物品对换即可。
将人和物对调并不会总是得到有价值的结果,但大多数情况有助于做出有意义的对比,比如为了向不同个体推荐商品,可能会先收集人们的购买历史,将商品与人对调——可以令零售商找到购买某些商品的潜在客户。另一个潜在用途则是在专门推荐链接的网站上,这样可以确保新出现的连接能够被那些最有可能对它产生兴趣的用户找到。
基于物品的过滤
从上面来看我们用的技术被称为“基于用户的协作过滤”,除此之外,另一种可供选择的方法被称为“基于物品的协作过滤”,在拥有大量数据集的情况下,基于物品的协作性过滤能够得出更好的结论,而且它允许我们将将大量计算任务预先进行,从而使给予推荐的用户能够更快地得到所要的结果。
其总体思路就是为每件物品预先计算好最为相近的其他物品。然后 当我们想为某位用户提供推荐时,就可以查看他曾经评过分的物品,从而选出排位靠前者,在构造出一个加权列表,其中包含了与这些选中物品最为相似的其他物品,这里与之前基于用户的最显著的区别在于,物品间的比较不会像用户间的比较那么频繁变化。这就意味着物品的相似度可以单独预先进行。
构造物品比较数据
为了对物品进行比较,我们要做的第一件事就是编写一个函数,构造一个包含相近物品的完整数据集,如下代码所示:
def calculateSimilarItems(prefs,n=10):
# Create a dictionary of items showing which other items they
# are most similar to.
result={}
# Invert the preference matrix to be item-centric
# 以物品为中心对偏好矩阵实施倒置处理
itemPrefs=transformPrefs(prefs)
for item in itemPrefs:
# Find the most similar items to this one
# 寻找最为相近的物品
scores=topMatches(itemPrefs,item,n=n,similarity=sim_distance)
result[item]=scores
return result
该函数首先利用了transformPrefs函数,对反映评价值的字典进行倒置处理,从而得到一个有关物品及其用户评价情况的列表,然后程序循环遍历每项物品,并将转换了的字典传入topMatches函数中,求得最为近似的物品及其相似度评价值,最后返回一个包含物品及其相近物品列表的字典
获得推荐
现在我们可以在不遍历整个数据集的情况下,利用反映物品相似度的字典来给出推荐了。我们可以取到用户评价过的所有物品,找出相近的物品,并根据相似度对其进行加权,我们可以很容易地根据物品字典来得到相似度。下标给出了利用基于物品的方法寻找推荐的过程。这里不同于之前没有涉及所有评论者,而是给出了一个表格,对我们打过分和未打过分的影片进行了对比。如下表所示:
每一行列出了一部曾经看过的电影,以及对该电影的个人评价。对于每一部未看过的电影,相应有一列与已观看影片的相似度,通过评分与相似度相乘来获取未看影片的推荐值。总计一行给出了每部影片相似度的总计与推荐值得总计。为了预测我们对影片的评分情况,只要将R.x列的总计值除以相似度总计值即可。我们可以看出来我们再也不必为所有其他评论者计算相似度评价值,因为物品相似度数据集是已经事先构造好的,如上面的calculateSimilarItems方法计算所得,代码如下所示:
基于物品的相似度获取推荐物品
def getRecommendedItems(prefs, itemMatch, user):
userRatings=prefs[user]
scores={}
totalSim={}
# Loop over items rated by this user
# 循环遍历由当前用户评分的物品
for(item, rating) in userRatings.items():
# Loop over items similar to this one
# 循环遍历与当前物品近似的物品
for (similarity,item2) in itemMatch[item]:
# Ignore if this user has already rated this item
# 忽略已经评价过的商品
if item2 in userRatings:
continue
scores.setdefault(item2,0)
# Weighted sum of rating times similarity
# 评价度与相似度加权之和
scores[item2]+=similarity*rating
totalSim.setdefault(item2,0)
# Sum of all the similarities
# 全部相似度之和
totalSim[item2]+=similarity
# Divide each total score by total weighting to get an average
# 加权合计值除以加权和,求出平均值,即为预计评分
rankings=[(score/totalSim[item],item) for item,score in scores.items( )]
# Return the rankings from highest to lowest
rankings.sort( )
rankings.reverse( )
return rankings
跟我来实战1:
学了这么多,该是那现实的数据练练手的时候了,我们使用的数据是来自于这个网站MovLen,
请大家下载10万的zip包即可。解压后里面有不少文件,但是我们只需要关心u.item和u.data,前者包含了一组影片id和影片信息的列表,后者则是包含如下形式(用户id、影片id、评分、评价时间)的实际评价情况:
该数据集包含了943位用户对1682部影片所做的评价,每位用户至少曾经为20部影片做过评价。那我们开始:
第一步 加载数据
def loadMovieLens(path='/data/movielens'):
# Get movie titles
movies={}
for line in open(path+'/u.item'):
(id,title)=line.split('|')[0:2]
movies[id]=title
# Load data
prefs={}
for line in open(path+'/u.data'):
(user,movieid,rating,ts)=line.split('\t')
prefs.setdefault(user,{})
prefs[user][movies[movieid]]=float(rating)
return prefs
第二步查看用户评分
>>> reload(recommendations)
<module 'recommendations' from 'recommendations.py'>
>>> prefs=recommendations.loadMovieLens()
>>> prefs['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 (199...
... ...
第三步获取基于用户的推荐
>>> recommendations.getRecommendations(prefs, '87')[0:10]
[(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, 'Ma
rlene 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)'), (4.815019082242709, 'Lette
r From Death Row, A (1998)')]
第四步获取基于物品的推荐
>>> itemsim=recommendations.calculateSimilarItems(prefs, n=50)
100 / 1664
200 / 1664
300 / 1664
。。。
>>> recommendations.getRecommendedItems(prefs, itemsim, '87')[0:30]
[(5.0, "What's Eating Gilbert Grape (1993)"), (5.0, 'Vertigo (1958)'), (5.0, 'Us
ual 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, 'Sli
ng Blade (1996)'), (5.0, 'Silence of the Lambs, The (1991)'), (5.0, 'Shining, Th
e (1980)'), (5.0, 'Shine (1996)'), (5.0, 'Sense and Sensibility (1995)'), (5.0,
'Scream (1996)'), (5.0, 'Rumble in the Bronx (1995)'), (5.0, 'Rock, The (1996)')
尽管物品相似度字典花费时间较长,但推荐过程中由于数据完全预先构造是瞬间完成,而且获取推荐所花费的时间不会随着用户数量增加而增加
基于用户过滤还是基于物品过滤
在针对大数据集生成推荐列表时,基于物品进行过滤的方式明显要比基于用户的过滤更快,不过它有维护物品相似度表的额外开销。影评数据相对密集(影评人几乎对每部电影都做过评价),而delicious书签用户的数据可能就是稀疏的(大多数书签都分散在小众收藏夹),对于稀疏数据,基于物品的过滤方法通常优于基于用户过滤,而对于密集数据,则两者效果几乎一样。尽管如此,基于用户的过滤方法更加易于实现,而且没有额外步骤,因此他更适用于规模小的变化非常频繁的内存数据集。总之,告诉一些人存在一些人和自己偏好相近是有一定价值的。
现在大家应该对相似度评价值的计算有所掌握,并且也应该清楚利用它们对用户和物品进行比较。