数据挖掘笔记十一: 协同过滤 & Slope One 算法

很久以前,我们上一些新闻网站,购物网站点过一些页面后,就会不停的有类似“相似推荐”的东西,我们基本上是不会点进去看的,因为推荐的太水了,不费脑子想就知道是基于一些标签的推荐,毫无新意。

现在的广告就有了很大进步,很多广告看上去就是 为你而设 的。关于推荐算法,最多使用的是 协同过滤。 最简单的协同过滤算法是 Slope One

今天就自己不用框架,纯手打一个 Slope One 算法出来!:grin:

Slope One算法

如下数据,已经三个用户对三部电影在豆花网上的评分如下,问号是因为他们没有评分,所以暂时没有,那么:求问号内的值。(也就是他们如果看了电影再来评,可能会评多少分)?

用户 丑人鱼 东游 长江8号
用户1 3 5 2
用户2 4 3
用户3 2 5

原理推演:

  1. 提取其它对<东游>有评价的用户的数据, 用户1,用户2的数据(取一列)[东游, 丑人鱼] [5,3],[3,4]
  2. 相加[5+3, 3+4] = [ 8, 7 ] 8-7 = 1,由于1这个差值是2个用户贡献出来的,所以除以2 = 0.5
  3. 那么预测用户3对<东游>的评分就是 2+0.5 = 2.5
  4. 重复到过程1,再取一列 [ 东游, 长江8号 ] 由于用户2没有数据,就得到了用户1 [5,2]
  5. 那么这个差值是5-2 = 3,由于是1个用户贡献出来的,所以除以1 = 3
  6. 那么再次预测用户3对<东游>的评分就是 5+3 = 8
  7. 那么根据加权平均算,根据已经参考过的数据算 (2.5*2 + 8*1) / (2 + 1) =4.3
  8. 结论:用户3对<东游>电影打分可能是4.3分
  9. 继续重复过程,可以将其它未打分的项都可以做预测了。

代码实现

了解了上面的过程原理,按思路写算法就行了,由于 Sklearn 工具包中没有相应的算法模块,所以自己手写一个即可。

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])           

使用

s = SlopeOne()
s.update(dict(alice=dict(squid=1.0, cuttlefish=4.0),
      bob=dict(squid=1.0, ,cuttlefish = 1.0,octupus=3.0)))
prediction = s.predict(dict(squid=3.0, cuttlefish=4.0))
for item,rating in prediction.iteritems():
    print item+":\t"+str(rating)

其它协同过滤算法

实际应用中,有很多推荐系统未必适用Slope One,基本可以根据业务场景自行选择,有些基于用户数据,有些基于物品数据,也有很多多次聚类的,根据具体场景不同,分析的方式也会有差异。