python实现SlopeOne

  • Post author:
  • Post category:python


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的实现过程,大家可以参考: