Slope One 算法是由 Daniel Lemire 教授在 2005 年提出。本文主要是对http://www.serpentine.com/blog/2006/12/12/collaborative-filtering-made-easy/的翻译,对源代码添加了中文注解,希望大家对SlopeOne代码理解更加深入:
数据格式
首先,介绍SlopeOne接收的数据格式:例如,Alice 给 squid 的评分为 1.0,给 cuttlefish 的评分为 4.0,bob 给squid 评分 1.0,cuttlefish 评分1.0,octopus 评分 3.0。很容易,在Python中实现,代码如下:
userdata = dict(alice = dict(squid = 1.0, cuttlefish = 4.0),
bob = dict(squid = 1.0, octopus = 1.0,cuttlefish = 3.0))
将 userdata 打印一下,简单理解即:一个字典中又包含多个字典,第一个字典的键值是user名称,value值是一个字典,暂且称之为子字典/second-level dict,并且,用户的个数决定了子字典的个数。每一个子字典,键值为 item名称,value值为 评分数据/rating。本例中,第一个字典包含两条数据,分别是alice及其对应的子字典,与bob及其对应的子字典。 每一个子子字典中包含对 squid的评分 与 对cuttlefish的评分或对 octupus的评分。
for user, prefs in userdata.iteritems():
for item,rating in prefs.iteritems():
print user + ":\t" + item + "\t" + str(rating) + "\n"
SlopeOne类
本文重点,SlopeOne类中包含update与predict两个方法,我都添加了注释,建议自己使用例子中的数据,自己走一遍流程,就很容易理解了:
class SlopeOne(object):
#self.diffs 矩阵存储评分矩阵,
#self.freqs 存储一对 items 被相同用户评分的数量。
def __init__(self):
self.diffs = {}
self.freqs = {}
# 根据提供的数据,构建self.diffs / self.freqs字典
def update(self, data):
# 遍历每个用户的每个评分数据
for user, prefs in data.iteritems():
# 确保子字典存在
for item, rating in prefs.iteritems():
self.freqs.setdefault(item, {})
self.diffs.setdefault(item, {})
# setdefault 作用:
#如果对于给定的键值/setdefault的第一个参数,
#字典中为对应value为空,
#则将setdefault的第二个参数赋值给它。
# 下面再次循环遍历user对应的prefs中的每一组评分
for item2, rating2 in prefs.iteritems():
self.freqs[item].setdefault(item2, 0)
self.diffs[item].setdefault(item2, 0.0)
# 使用整数0初始化次数,浮点型零初始化评分。
# 利用两个item是否同时被一个用户评分,
#对self.freqs进行更新
self.freqs[item][item2] += 1
# 利用两个item的评分之差,对self.diffs矩阵进行更新
self.diffs[item][item2] += rating - rating2
# 将两个item在diffs 矩阵与 freqs矩阵对应位置相除,
#结果保存到freqs中,即为两个item的平均差距
for item, ratings in self.diffs.iteritems():
for item2, rating in ratings.iteritems():
ratings[item2] /= self.freqs[item][item2]
# 对新的用户偏好,根据 self.diffs / self.freqs 对新用户进行评分预测
def predict(self, userprefs):
# 定义两个空字典,preds存储预测数据,freqs存储计数
preds, freqs = {}, {}
# 迭代每一个物品(被用户评分过的)
# 使用try/except跳过没有被评分的物品对
for item, rating in userprefs.iteritems():
for diffitem, diffratings in self.diffs.iteritems():
try:
freq = self.freqs[diffitem][item]
except KeyError:
continue
# 设置preds初始值为0.0, freqs初始值为0
preds.setdefault(diffitem, 0.0)
freqs.setdefault(diffitem, 0)
# 累加
preds[diffitem] += freq * (diffratings[item] + rating)
freqs[diffitem] += freq
# 在返回结果之前,进行过滤
# 返回一个 带权重预测值 的新字典
# 结果中除去了 用户已经评分过的内容 和 物品计数为零的内容
return dict([(item, value / freqs[item]) for item, value in preds.iteritems() if item not in userprefs and freqs[item] > 0])
实际应用
接下来,使用 update 将数据读入矩阵。 注意到,update 函数,没有用到user IDs。Slope-one 不关心谁进行了评分,只关心一个物品被评的分数,这种技术称为“基于物品的协同过滤”。使用alice、bob的数据进行diffs、freqs的建立。
s = SlopeOne()
s.update(dict(alice=dict(squid=1.0, cuttlefish=4.0),
bob=dict(squid=1.0, ,cuttlefish = 1.0,octupus=3.0)))
进行预测时候,我们传入一个用户的评分给predict方法,它会输出用户没有评分过的物品的预测评分。
prediction = s.predict(dict(squid=3.0, cuttlefish=4.0))
for item,rating in prediction.iteritems():
print item+":\t"+str(rating)
总结
通过观察update方法中创建矩阵的方式,发现潜在的节省空间的发现。
- 每个矩阵的一些部分完全空的,他们不包含零,他们什么都没有,例如Alice对squid,cuttlefish给出评分,Bob给squid、octopus给出评分,那么矩阵中没有cuttefish与octopus这项。
- self.diffs矩阵对角线位置总是为零或者没有值,所以无需存储self.freqs矩阵对角线元素。
- self.diffs矩阵右上角元素是左下角元素的相反数.self.freqs右上角元素与左下角元素是对称的。
Slope One 优点:
使用严格的下三角矩阵表示self.diffs 与self.freqs,节省空间。
Slope One容易实现,原理简单易懂。
updates/predictions方法都可以使用并行方式实现,predictions容易使用map-reduce表示。
更好的是,Slope One可以在线上更新数据结构,而不需要离线处理大型数据库。
最后,建议将class SlopeOne存放到slope_one.py文件中,方便调用。
第一次写博客,欢迎大家前来拍砖!
参考
后两篇博文有助于理解SlopeOne的实现过程,大家可以参考: