这里所有代码都是由Python实现!

一个协作性过滤算法通常的做法就是对一大群人进行搜索,从中找出来和我们品味兴趣相近的一小群人来。

 

  推荐算法,从字面上看就是向用户推荐他所感兴趣的内容,如果是购物网站,就推荐他感兴趣的商品;如果是音乐网站,就推荐他感兴趣的音乐等等。

说到推荐算法,我最先能想到的就是相似度计算,但是如何应用呢?而这里又谈到计算,就要有数,那数从哪里来呢?

  由刚才提到的协作性过滤算法,可以知道如果要提供推荐,那就要获取大量的数据,包括人,商品以及人对商品的评价,通过分析评论来对用户进行推荐,

将用户评论量化就得到了数,有了数就可以进行计算了。

  这里我要说两种推荐算法:其一是基于用户的推荐算法,其二是基于物品或商品的推荐算法。这两种是类似的,只是将物品和人互换一下的感觉。

首先说一下两种算法的思路:

  对于基于用户的推荐算法,由名字可以知道,它就由人来进行物品的推荐的,找到与被推荐用户品味兴趣相同的用户,根据

这些用户来找到推荐的商品。当数据量非常大的时候,基于用户的推荐算法,效率就比较低了,因为每次为某个用户进行推荐,都要将其与其余所有用户

计算相似度。

  因此出现了基于物品的推荐算法,顾名思义,它是找到每件物品最为相近的物品,也就是对物品之间计算相似度,当为某位用户推荐时,查看他

价过的物品或者购买过的物品来为他推荐相近的可能是他感兴趣的物品。

  基于物品的推荐算法和基于用户的推荐算法相比,最显著的区别在于,物品间的比较不会像用户间的比较那么频繁变化。这就表示不用不停的计算

与每样物品最为相近的其他物品:,而基于用户的推荐算法,计算某个用户的相似用户,就要拿该用户去和其他所有用户去计算相似度,这就显得很麻烦了。

  白话介绍完以上两个推荐算法之后,该正式说一下推荐算法的主要思路了:

  一、以下步骤以基于用户的推荐算法为例讲解思路。

  1、首先要收集到一群人对相应的物品或商品评价值,并将其保存起来。这里我用字典的形式将这些数据保存起来,用一个二层字典的形式,

最外层字典的键为用户的名称,即人的名字,value值为一个第二层的字典,第二层字典包含的是商品和评价值的键值对。这里以一个影评者及其

对几部影片的评价值为例,即:

# 一个涉及影评者及其几步影片评分情况的字典

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}}

  2、有了数据,就可以开始运算了,这里要提到相似度计算,通过相似度计算可以得到相近的人或者相近的物,然后再向用户进行推荐。相似度计算有很多种,这里我主要

说其中两种相似度计算的方法:(1)欧几里德距离;(2)皮尔逊相关系统

  (1)欧几里德距离:它是要构建一个“偏好”空间,“偏好”空间以人们一致评价的物品或商品为坐标轴,将参与评价人绘制到“偏好”空间里。然后计算人与人之间的距离,距离越

近,则表示他们的兴趣偏好越相近。这里面的距离即为坐标轴中两点之间的距离计算公式是一样的,即对两点各个对应坐标的差值平方和进行开根号。具体代码表示如下:

from math import sqrt

# 返回一个person1和person2的基于距离的相似度评价,即:欧几里德距离
# 其中prefs所要传入的参数为第一步建立的字典
def sim_distance(prefs, person1, person2):
    # 由欧几里德距离的定义可知,首先要找到两个人一致评价的物品或商品
    sim_items = []
    for item in prefs[person1]:
        if item in prefs[person2]:
            sim_items.append(item)

    # 若没有一致评价的物品,则相似度为0
    if len(sim_items) == 0:
        retunr 0

    # 如果有一致评价的物品,则使用欧几里德距离计算相似度
    sim = 0
    # 计算差值平方和
    for item in sim_items:
        sim += pow(prefs[person1][item] - prefs[person2][item], 2)
  
return 1 / (1 + sqrt(sim))

    最后返回的结果为1 / (1 + sqrt(sim)),这是因为把相似度变为0——1之间的数,越大越相似,越小越不相似。其中计算差值平方和也可以直接用一个列表推导式来表达。

  (2)皮尔逊相关系统(皮尔逊相关度评价):它是判断两组数据与某一直线拟合程度的一种度量。与欧几里德距离相反,它是以人为坐标轴,将人们一致评价过的物品或商

品绘制到坐标系里的。对应的计算公式相比欧几里德距离要复杂的多,而且难于理解。具体如何理解等我能很好的理解了再给出分析。皮尔逊相关系统主要是在数据不是很规范

的时候,会倾向于给出更好的结果。刚才第一句说,皮尔逊相关系数是判断两组数据与某一直线拟合程度的一种度量,这条直线也会绘制到坐标系中,它的绘制原则是尽可能地

靠近图上的所有坐标点,故而成为最佳拟合线。

   在这里,我要说一下,皮尔逊相关系数计算很复杂,但为什么会提出皮尔逊相关系数呢?其中最主要的原因是皮尔逊相关系数解决了“夸大分值”的情况。什么又是“夸大分值”

的情况呢,举个例子来说,对于有一种情况,就是对于两个人都评价的商品或物品,如果其中一个人的评价值始终高于另一个人,而且两者对同一影片的评价值之差也非常的相近,

这样两个人的品味其实是相似的,他们的最终直线也仍然是拟合的。但是如果将这两个人用欧几里德距离去计算,他们的相似度会偏低,这就是所谓的“夸大分值”的情况。

  接下来给出皮尔逊相关系统的代码,代码中包含它的计算方法:

# 对于皮尔逊相关系统,也是计算两个人的相似度,同时也要找到两个# 人共同评价过的商品或者物品,其中prefs也是之前第一步中提出的# 字典
def sim_pearson(prefs, person1, person2):
    # 首先获取两个人都评价过的物品列表
    sim_items = []
    for item in prefs[person1]:
        if item in prefs[person2]:
            sim_items.append(item)

    # 若两者无共同评价过的商品,则相似度为0
    n = len(sim_items)  # 这个n在计算时要用到
    if n == 0:
        return 0

    # 若两者有共同评价的商品,则通过皮尔逊相关度来计算相似度
    # 首先对每个人的偏好求和
    sum1 = 0
    sum2 = 0
    for item in sim_items:
        sum1 += prefs[person1][item]
        sum2 += prefs[person2][item]

    # 然后求每个人的偏好平方和
    sum1Sq = 0
    sum2Sq = 0
    for item in sim_items:
        sum1Sq += pow(prefs[person1][item], 2)
        sum2Sq += pow(prefs[person2][item], 2)

    # 求两个人偏好乘积之和
    pSum = 0
    for item in sim_items:
       pSum = prefs[person1][item] * prefs[person2][item]

    # 准备工作做完,最后计算皮尔逊相关系数
    num = pSum - (sum1 * sum2 / n)
    den = sqrt((sum1Sq - pow(sum1, 2) / n) * (sum2Sq - pow(sum2, 2) / n))
    if den == 0:   
    # 皮尔逊相关系数是num/den,若den为0就无意义了,返回0
        return 0
    
    return num / den

    由代码可知,皮尔逊相关系数的计算比较难,但是相对于某些情况来说(如“夸大分值”的情况),皮尔逊相关系数的效果是很好的。皮尔逊相关系数计算的值为一个介于-1到1之间的数,

若大于0,则表示两个人的评价值呈现正相关,若小于0,则说明两个人的评价值呈现负相关。值为1则表明两个人对每一样物品均有着完全一致的评价。

  3、有了相似度度量方法,则可以通过字典数据来计算人与人之间的相似度了(也可以计算物品与物品之间的相似度,但需要对字典数据进行相应的转换,转换为物品,人和评价值的字典),

这里写一个函数来计算相似度并返回top-n最相近的人或物。

# 用来计算某人与其他人的相似度,并返回最相似的前几个
# 其中prefs为字典数据,要找到与person相似的人
# n为返回的前n个最相似的人,similarity为相似度度量方法
# similarity可以用皮尔逊也可以用欧几里德,也可用其他相似度方法
# similarity默认用皮尔逊度量方法
def topMathches(prefs, person, n, similarity=sim_pearson):
    sims = [] #将相似度和人构建元组存到列表里,便于排序 
    for otherperson in prefs:
        # 首先判断otherperson是不是person,避免自己同自己比较
        if otherperson != person:
            sim = similarity(person, otherperson)
            # 为了方便排序,将sim当做键,otherperson当做value
            sims.append((sim, otherperson))

    sims.sort()
    sims.reverse()
    return sim[0: n]

    通过该函数就可以得到与person最相似的前n个人了。该方法也可以将person替换为物品,prefs转换为物品,人和评价值的字典形式,然后来计算某个物品最相近的几个物品。

  4、现在我们可以得到与被推荐用户最为相似的几个用户了,但是这还没有达到我们的目的,我们的目的是为了向被推荐用户推荐其感兴趣的商品或者物品,因此这一步是要为用户推荐物品。

怎么推荐呢?其中有中方法是,可以为其推荐相似用户所评价或购买的一件物品,而这件物品被推荐用户没有评价或购买这件物品,但是这样显得很随意,因为可能会有问题:以电影评价为例,

评论者还未对某些影片做过评论,而这些影片也许就是被推荐用户所喜欢的;还有一种可能,会找到一些热衷某些影片的古怪评论者,但根据上一步topMatches返回的结果,可能其他的评论者

都不看好这部影片。为了解决以上可能出现的问题,要生成一个适合任何情况的推荐方法,这里通过一个经过加权的评价值来为影片打分,评论者的评分结果因此形成了先后的排名。如何加权:

我们得到与一些评论者的相似度之后,用相似度去乘以他们为每部影片所给的评价值,这样一来,相比于与被推荐用户不相近的人,那些与被推荐用户相近的人将会对整体评价值拥有更多的贡献,

对一部影片,将这些乘积相加的一个总计值,但是如果这部影片被更多的人评价的话(相比于其他电影这部电影评价的人更多),就会导致对总计值的结果造成很大的影响,因此我们拿这个总计值

去除以参与评价这部影片的评价者的相似度之和。这就是加权的方法。下面我们用图表显示,并做大概说明:

                      表:为Toby提供推荐

评价者\影片名称 相似度 Night  S.xNight  Lady S.xLady  Luck S.xLuck
Rose 0.99 3.0 2.97 2.5 2.48 3.0 2.97
Seymour 0.38 3.0 1.14 3.0 1.14 1.5 0.57
Puig 0.89 4.5 4.02     3.0 2.68
LaSalle 0.92 3.0 2.77 3.0 2.77 2.0 1.82
Matthews 0.66 3.0 1.99 3.0 1.99    
总计     12.89   8.38   8.07
Sim. Sum     3.82   2.95   3.18
总计/Sim. Sum     3.35   2.83   2.53

  其中最左边一列表示评价者以及总计值和所有评价者相似度之和,最上面一行位影片名称,已经经过加权的影片名称(即S.x***)。上面的话用公式来解读就是:

由表名可知,是为Toby推荐影片,那首先要计算其他评论者与Toby的相似度,然后为其推荐他没有评价过的影片,由表知,需要推荐的影片是Night,Lady,Luck;这里以一部影片

Lady为例子,为要推荐的影片做排名,按照上述所说加权的方法,为:

  评价Lady的评价者只有4个,因此首先计算总计值,总计值 = 2.5 * 0.99 + 3.0 * 0.38 + 3.0 * 0.92 + 3.0 * 0.66 = 8.38;然后计算Sim. Sum,即参与评价该影片的评价者的相似度之和,

即为0.99 + 0.38 + 0.92 + 0.66 = 2.95;最后为影片打分为:总计值/Sim. Sum = 8.38 / 2.95 = 2.83。通过计算示例则可以对该方法一目了然,对影片的排名即推荐写为一个函数形式:

# 返回为person推荐的物品,prefs为第一步的字典数据
# n为推荐的个数(物品排名前n名),
# similarity指的是相似度度量方法,这里默认为皮尔逊相关系数
def getRecommendations(prefs, person, n, similarity=sim_pearson):
    # 以要推荐的影片为准(这些推荐的影片都是person未评价的)
    totals = {}  # 用来存放总计值,键值为 影片名:总计值
    simSum = {} # 用来存放参与评价相似度之和,键值为 影片名:Sim. Sum
    
    # 计算相似度不能拿person自己去计算相似度,因此过滤person
    for otherperson in prefs:
        if otherperson != person:
            sim = similarity(prefs, person, otherperson)
        else:
            sim = 0 #为了过滤掉自己和自己计算相似度的情况
      
        # 由于使用皮尔逊相关系数,因此要去掉小于等于0的相似度
        if sim > 0:
            for item in prefs[otherperson]:
                # 过滤掉person评价过的影片
                if item not in prefs[person]:
                    totals.setdefault(item, 0)
                    totals[item] += prefs[otherperson][item] * sim
                    simSum.setdefault(item, 0)
                    simSum[item] += sim       

    # 经过上面的for循环,则得到了需要被推荐的影片的总计值以及相应的参与评价的相似度之和
    rankings = [] # 用来保存推荐的影片及其排名,影片和排名构成元组存到列表中,便于排序
    for item in totals:
        rankings.append((totals[item]/simSum[item], item))# 将影片排名值放在前面,方便排序
    rankings.sort()
    rankings.reverse()
    return rankings[0: n]
               

    这个函数里用了一个setdefault函数,setdefault(item, 0)是在字典里创建一个item:0的键值对,如果字典中包含item,则这条语句不起任何作用,如果不包含item,则创建item:0。

    将上面的所有函数结合起来,就可以得到为某个人推荐的物品及其排名了。

 

  以上思路主要是以基于用户来推荐的,因此是基于用户的推荐算法的整体流程,基于物品的推荐算法的流程又是什么呢,其实很简单,只要把数据字典换成“{物品:{评价者:评价值}}”即可。通过一段代码来将之前的数据转换为物品的形式:

# prefs为{评价者:{物品:评价值}}的字典形式
# 返回{物品:{评价者:评价值}}的字典形式
def transformPrefs(prefs):
    itemPrefs = {}
    for person in prefs:
        for item in prefs[person]:
           itemPrefs.setdefault(item, {})  # 使用setdefault的特点,很容易进行字典的转换
           # 将物品和人员对调
           itemPrefs[item][person] = prefs[person][item]

    return itemPrefs   

  利用这段代码,将字典形式转换之后,然后就可以处理基于物品的推荐算法了。

  在这里要再大致说下两个算法:基于用户的推荐算法是找到相似的用户,然后根据相似用户对影片的评价数据来进行推荐的;而基于物品的推荐算法是

要找到相似的物品,然后根据被推荐者参加评价的物品或者购买过的物品的评价数据来进行推荐(根据评价记录或者购买记录)

  

   二、基于物品的推荐算法

     以上思路的讲解或多或小有涉及到两者的不同之处,这里总结一下两者的不同。

     两种算法的区别:

     区别一:两种算法的数据形式不一样,基于用户的推荐算法的数据形式为:{评价者:{物品:评价值}},而基于物品的推荐算法的数据形式为:{物品:{评价者:评价值}};

     区别二:在以上所有的函数中的计算相似度只是将person和prefs替换为item(物品)和newprefs

     区别三:基于用户的推荐算法只要求出被推荐用户的相似用户即可,而基于物品的推荐算法则需要计算出每件物品的相似物品,构建一个物品相似表,则需要构建新的函数

     来得到物品相似表。

     区别四:基于用户的推荐算法是利用其它相似用户的评价数据来进行推荐;而基于物品的推荐算法是利用被推荐用户的历史评论数据来进行推荐的。因此在获得推荐的函数                     中,基于物品的推荐算法,表的最左边一列是被推荐用户的历史评价物品名称(而基于用户推荐,表的最左边一列是相似用户名称)。

     区别五:两种算法应用的情况不一样,在针对大数据集生成推荐列表时,基于物品进行过滤的方式明显要比基于用户的过滤更快,不过它也有维护物品相似度表的额外开                         销。总体来说,对于稀疏数据集,基于物品的过滤方法通常要优于基于用户的过滤方法;而对于密集数据集,两者的效果几乎是一样的。但是,基于用户的过滤方法更容易                     实现,而且无需额外步骤,因此它通常适用于规模较小的变化非常频繁的内存数据集。

 

  • 由区别一以及转换字典函数可以得出基于物品的推荐算法所需要的数据形式,将第一章第一步的字典critics代入转换函数返回的结果即为所需的数据形式。

  • 再由区别二以及相似度度量方法,将相似度度量函数中的参数person1和person2替换为item1和item2即可;

  • 再根据区别三,写一个获得物品相似表的函数,通过该代码则可以得到一个物品相似表,用字典形式保存。代码如下:

# prefs指的是{评价者:{物品:评价值}}字典数据
# n指的是要求出每件物品的前n个相似物品
def calculateSimilarItems(prefs, n=10):
    # 建立输出字典,以给出与这些物品最为相近的所有其他物品
    simItems = {}
    
    # 使用转换算法将数据进行转换
    itemPrefs = transformPrefs(prefs)
    
    c = 0  # 主要是针对大数据集的,用来计数(不影响物品相似表的获取)
    for item in itemPrefs:
        # 针对大数据集更新状态变量
        c += 1
        if c % 100 ==0:
            print("%d / %d" % (c, len(itemPrefs)))
        # 以上三条语句与物品相似表的获取无关
        
        scores = topMatches(itemPrefs, item, n)
        simItems[item] = scores

    return simItems

  • 再根据区别四可知,为某个用户推荐物品,需要根据他的历史评价记录数据或历史购买记录数据来获得推荐的物品数据。相较于基于用户的推荐算法的加权计算表,基于物

  品的加权计算表格为如下所示:

                表:为Toby推荐物品(基于物品的推荐)

影片名称 评分 Night R.xNight Lady R.xLady Luck R.xLuck
Snakes 4.5 0.182 0.818 0.222 0.999 0.105 0.474
Superman 4.0 0.103 0.412 0.091 0.363 0.065 0.258
Dupree 1.0 0.148 0.148 0.4 0.4 0.182 0.182
总计   0.433 1.378 0.713 1.762 0.352 0.914
归一化结果     3.183   2.473   2.598

  表中最左边一列表示Toby曾经评价过的影片(也就是历史评价记录),而表最上面一行指的是Toby未曾评价过的影片以及这些影片的加权值(即R.x****)。以Night为例,Night

  的排名为:总计值 = 0.182 * 4.5 + 0.103 * 4.0 + 0.148 * 1.0 = 1.378,相似度之和sim. Sum = 0.182 + 0.103 + 0.148 = 0.433,最后排名结果为:1.378 / 0.433 = 3.183。直观

  地从两种算法的表可以看出,基于物品的推荐是固定被推荐者的参与评价的几个影片的评价值,而未曾评价过的的影片(需要推荐的影片)则根据不同的影片相似度是不同的;

  而基于用户的推荐是固定被推荐者与几个相似者的相似度,而未曾评价过的的影片(需要推荐的影片)则根据不同的评价者评价值是不同的。

  随后根据表格,写出推荐物品的函数,代码如下

# 其中prefs为{评价者:{物品:评价值}}的字典数据,目的是为了找到被推荐用户user的评价记录(即得到user所评价过的物品)
# simItems指的是相似物品表,是calculateSimilarItems的返回结果
def getRecommendationsByItems(prefs, simItems, user):
    # 首先获取user的评价表
    userItems = prefs[user]
    totals = {} # 存储 item:总计值
    simSum = {} # 存储 item:相似度之和

    # 找到user评价过的每个物品的相似物品
    # item为物品,rating为对应的评价值
    # userItems.items()返回一个列表,列表元素为(物品, 评价值)
    for (item, rating) in userItems.items():
        # 循环遍历与当前物品item相近的物品
        # simItems[item]就是一个元素为(物品,评价值)的列表
        for (similarity, item2) in simItems[item]:
            # 要过滤掉user已经评价过的物品,保留未评价过的
            if item2 not in userItems:
                totals.setdefault(item2, 0)
                simSum.setdefault(item2, 0)
                totals[item2] += rating * similarity
                simSum[item2] += similarity

    # 求出一个推荐的排名
    rankings = []
    for r_item in totals:
        rankings.append((totals[r_item] / simSum[r_item], r_item))
    return rankings    

    

 

 

  以上所有内容即为两个推荐算法:基于用户的推荐和基于物品的推荐。它们两者的使用原则之前也说过,现在重新说一下:在针对大数据集生成推荐列表时,基于物品进行过滤的方式明显要比基于用户的过滤更快,不过它也有维护物品相似表的额外开销。总体来说,对于稀疏数据集,基于物品的过滤方法通常要优于基于用户的过滤方法;而对于密集数据集,两者的效果几乎是一样的。但是,基于用户的过滤方法更容易实现,而且无需额外步骤,因此它通常适用于规模较小的变化非常频繁的内存数据集。