python -- 面向程序员的数据挖掘指南-推荐系统入门-分类-005
在上几章中, 无论显示评价还是隐式评价, 我们都使用的是用户对物品的评价来进行推荐,这一章我们将使用物品本身的特征来进行推荐。
比如你购买了Phoenix专辑,购买过这张专辑的用户还会去购买Vampire的专辑,因此会把它推荐给你;我在Netflix上观看了Doctor Who,网站会向我推荐Quantum Leap,用的是同样的原理。
这一章我们来看另一种推荐方法。
以潘多拉音乐站举例,其实在播放界面上可以看到推荐理由:
“根据你目前告知的信息,我们播放的这首歌曲有着相似的旋律,使用了声响和电音的组合,即兴的吉他伴奏。” 潘多拉网站的推荐系统是基于一个名为音乐基因的项目。
他们雇佣了专业的音乐家对歌曲进行分类(提取它们的“基因”)。其实这些基因就是歌曲的特征。
特征值选取的重要性
你知道吗? 潘多拉网站为歌曲甄别出400多种特征。
假设潘多拉会用曲风和情绪作为歌曲特征,分值如下:
曲风:乡村1分,爵士2分,摇滚3分,圣歌4分,饶舌5分
情绪:悲伤的1分,欢快的2分,热情的3分,愤怒的4分,不确定的5分
比如James Blunt的那首You're Beautiful是悲伤的摇滚乐,用图表来展示它的位置便是:
比如一个叫Tex的用户喜欢You're Beautiful这首歌,我们想要为他推荐歌曲。
我们的歌曲库中有另外三首歌:歌曲1是悲伤的爵士乐;歌曲2是愤怒的圣歌;歌曲3是愤怒的摇滚乐。你会推荐哪一首?
也许你已经看出了这种算法中的不足,乡村和爵士的距离是1,乡村和摇滚的距离是2,悲伤的乐曲和快乐的乐曲是相近的等等。 即使调整了分值的分配,也不能解决问题。这就是没有选取好特征值的例子。
乡村、爵士、摇滚、圣歌、饶舌这些类型应该是等价的。因此我们将每种歌曲类型拆分成单独的特征,并对此进行打分:
“乡村(Country)”一栏的1分表示完全不是这个乐曲风格,5分则表示很相符。这样一来,评分值就显得有意义了。如果一首歌的“乡村音乐”特征是4分,另一首是5分,那我们可以认为它们是相似的歌曲。
其实这就是潘多拉所使用的特征抽取方法。每个特征都是1到5分的尺度,0.5分为一档。特征会被分到不同的大类中。通过这种方式,潘多拉将每首歌曲都抽象成一个包含400个数值元素的向量,并结合我们之前学过的距离计算公式进行推荐。
简单的示例
我们先来构建一个数据集,我选取了以下这些特征(可能比较随意),使用5分制来评分(0.5分一档):
使用钢琴的程度(Piano):1分表示没有使用钢琴,5分表示整首歌曲由钢琴曲贯穿;
使用美声的程度(Vocals):标准同上
节奏(Driving beat):整首歌曲是否有强烈的节奏感
蓝调(Blues infl.)
电音吉他(Dirty elec. Guitar)
幕后和声(Backup vocals)
* 饶舌(Rap infl.)
然后我们便可以使用距离计算公式了,比如要计算Dr. Dog的Fate歌曲和Phoenix的Lisztomania之间的曼哈顿距离:9
music = {"Dr Dog/Fate": {"piano": 2.5, "vocals": 4, "beat": 3.5, "blues": 3, "guitar": 5, "backup vocals": 4, "rap": 1},
"Phoenix/Lisztomania": {"piano": 2, "vocals": 5, "beat": 5, "blues": 3, "guitar": 2, "backup vocals": 1, "rap": 1},
"Heartless Bastards/Out at Sea": {"piano": 1, "vocals": 5, "beat": 4, "blues": 2, "guitar": 4, "backup vocals": 1, "rap": 1},
"Todd Snider/Don't Tempt Me": {"piano": 4, "vocals": 5, "beat": 4, "blues": 4, "guitar": 1, "backup vocals": 5, "rap": 1},
"The Black Keys/Magic Potion": {"piano": 1, "vocals": 4, "beat": 5, "blues": 3.5, "guitar": 5, "backup vocals": 1, "rap": 1},
"Glee Cast/Jessie's Girl": {"piano": 1, "vocals": 5, "beat": 3.5, "blues": 3, "guitar":4, "backup vocals": 5, "rap": 1},
"La Roux/Bulletproof": {"piano": 5, "vocals": 5, "beat": 4, "blues": 2, "guitar": 1, "backup vocals": 1, "rap": 1},
"Mike Posner": {"piano": 2.5, "vocals": 4, "beat": 4, "blues": 1, "guitar": 1, "backup vocals": 1, "rap": 1},
"Black Eyed Peas/Rock That Body": {"piano": 2, "vocals": 5, "beat": 5, "blues": 1, "guitar": 2, "backup vocals": 2, "rap": 4},
"Lady Gaga/Alejandro": {"piano": 1, "vocals": 5, "beat": 3, "blues": 2, "guitar": 1, "backup vocals": 2, "rap": 1}}
计算曼哈顿距离
def manhattan(rating1, rating2):
"""Computes the Manhattan distance. Both rating1 and rating2 are dictionariesof the form {'The Strokes': 3.0, 'Slightly Stoopid': 2.5}"""
distance = 0
total = 0
for key in rating1:
if key in rating2:
distance += abs(rating1[key] - rating2[key])
total += 1
return distance
找出最近的歌曲,并且推荐
def computeNearestNeighbor(username, users):
"""creates a sorted list of users based on their distance to username"""
distances = []
for user in users:
if user != username:
distance = manhattan(users[user], users[username])
distances.append((distance, user))
# sort based on distance -- closest first
distances.sort()
return distances
def recommend(username, users):
"""Give list of recommendations"""
# first find nearest neighbor
nearest = computeNearestNeighbor(username, users)[0][1]
recommendations = []
# now find bands neighbor rated that user didn't
neighborRatings = users[nearest]
userRatings = users[username]
for artist in neighborRatings:
if not artist in userRatings:
recommendations.append((artist, neighborRatings[artist]))
# using the fn sorted for variety - sort is more efficient
return sorted(recommendations, key=lambda artistTuple: artistTuple[1], reverse = True)
假设我有一个朋友喜欢Black Keys Magic Potion,我便可根据曼哈顿距离来进行推荐:
print(computeNearestNeighbor('The Black Keys/Magic Potion', music))
[(4.5, 'Heartless Bastards/Out at Sea'), (5.5, 'Phoenix/Lisztomania'), (6.5, 'Dr Dog/Fate'), (8.0, "Glee Cast/Jessie's Girl"), (9.0, 'Mike Posner'), (9.5, 'Lady Gaga/Alejandro'), (11.5, 'Black Eyed Peas/Rock That Body'), (11.5, 'La Roux/Bulletproof'), (13.5, "Todd Snider/Don't Tempt Me")]
这里我推荐的是Heartless Bastard的Out as Sea,还是很合乎逻辑的。
评分标准
评分标准不一是所有推荐系统的大敌!
举个有趣的例子。在婚恋网站上,我通过用户的年龄和收入来进行匹配:
可以看出,年龄的最大差异是28,而薪资的最大差异则是72,000。因为差距悬殊,薪水的高低基本决定了匹配程度。
如果单单目测,我们会将David推荐给Yun,因为他们年龄相近,工资也差不多。
但如果使用距离计算公式,那么53岁的Brian就会被匹配给Yun,这就不太妙了。
面对这样的问题,不用担心,我们可以使用标准化。
标准化
要让数据变得可用我们可以对其进行标准化,最常用的方法是将所有数据都转化为0到1之间的值。
拿上面的薪酬数据举例,最大值115,000和最小值43,000相差72,000,要让所有值落到0到1之间,可以将每个值减去最小值,并除以范围(72,000)。这样所有的薪酬都落到了0和1之间。
如果你学过统计学,会知道还有其他的标准化方法。
比如说标准分(z-score)——分值偏离均值的程度:
但是标准分也不是完美的,它也存在一些问题。
标准分的问题在于它会受异常值的影响。
比如说一家公司有100名员工,普通员工每小时赚10美元,而CEO一年能赚600万,那全公司的平均时薪为:
# 100个员工每小时10美元+ 一年52周,一周40小时
a = (100*10+6000000/40/52)/101
print(a)
38.46153846153846
结果是每小时38美元,看起来很美好,但其实并不真实。鉴于这个原因,标准分的计算公式会稍作变化。
修正的标准分
计算方法:将标准分公式中的均值改为中位数,将标准差改为绝对偏差。
绝对偏差是每个数与中位数的差值求和,然后再平均得到的。
首先将所有人按薪水排序,找到中位数,然后计算绝对偏差:
# 最后,我们便可以计算得出Yun的修正标准分:
print((75000-69500)/19125)
0.2875816993464052
是否需要标准化?
当物品的每个特征数值尺度不一时,就有必要进行标准化。
比如我想在新墨西哥圣达菲买一处宅子,下表是一些选择:
可以看到,价格的范围是最广的,在计算距离时会起到决定性作用;同样,有两间卧室和有二十间卧室,在距离的影响下作用也会很小。
需要进行标准化的情形:
我们需要通过物品特性来计算距离; 不同特性之间的尺度相差很大。
过分的使用标准化,反而会降低结果正确性