2.1 概述

推荐系统最核心的部分是推荐算法,推荐算法的不同则会导致推荐系统中推荐的准确性发生偏差。协同过滤(Collaborative Filtering)算法是推荐系统中最早诞生的算法之一,并且在推荐系统领域也是使用最多最普遍的推荐算法,协同过滤的原理是首先给目标用户找出其相似用户,通过相似用户的喜好进而挖掘出目标用户的喜好

协同过滤算法的有两个主要的功能

  • 其一是找出目标用户的相似用户的过程,这个过程可以称为预测过程
  • 其二是相似用户的喜好挖掘目标用户的喜好,这个过程可以称为推荐过程

预测过程我们通过会使用目标用户的行为及日志等用户信息去挖掘目标用户的偏好模型,然后通过这个偏好模型再去找出相似带有该偏好模型的用户,而在推荐过程我们会采用这些带有相似偏好模型的用户喜好的物品推荐给目标用户。其实这个过程可以用日常生活中的例子来阐述,我们去看电影的时候,会经常遇到看什么电影的问题,当看到电影传单上所有的电影时,我们又很难确定看哪个电影,而这个时候我们通常会问朋友或同学,通过去询问或者打听看哪部电影,而通常问的这个朋友或者同学是兴趣爱好相似比较信任的。这个过程可以很生动的描述推荐系统的核心思想。

所谓协同过滤,基本思想根据用户之前的喜好以及其他兴趣相近的用户的选择来给用户推荐物品(基于对用户历史行为数据的挖掘发现用户的喜好偏向, 并预测用户可能喜好的产品进行推荐),一般是仅仅基于用户的行为数据(评价、购买、下载等), 而不依赖于项的任何附加信息(物品自身特征)或者用户的任何附加信息(年龄, 性别等)。目前应用比较广泛的协同过滤算法是基于邻域的方法, 而这种方法主要有下面两种算法

  • 基于用户的协同过滤算法(UserCF):给用户推荐和他兴趣相似的其他用户喜欢的产品
  • 基于物品的协同过滤算法(ItemCF):给用户推荐和他之前喜欢的物品相似的物品

不管是UserCF还是ItemCF算法,非常重要的步骤之一就是计算用户和用户或者物品和物品之间的相似度,所以下面先介绍常用的相似性度量方法,然后再对每个算法的具体细节进行展开。

对于协同过滤推荐算法,其流程如下图所示,主要分为三步:第一,收集用户偏好,第二,找到相似的用户和物品,第三,形成相似推荐

第二章 协同过滤(Collaborative Filtering) - 图1

2.2 相似性度量方法

2.2.1 Jaccard相似系数

Jaccard相似系数是衡量两个集合的相似度的一种指标,定义为两个用户u和v交互商品交集的数量占这两个用户交互商品并集的数量的比例。用符号第二章 协同过滤(Collaborative Filtering) - 图2表示,公式如下

第二章 协同过滤(Collaborative Filtering) - 图3%20%5Ccap%20N(v)%7C%7D%7B%5Csqrt%7B%7CN(u)%7C%20%5Ccup%7CN(v)%7C%7D%7D%0A#card=math&code=sim_%7Buv%7D%3D%5Cfrac%7B%7CN%28u%29%20%5Ccap%20N%28v%29%7C%7D%7B%5Csqrt%7B%7CN%28u%29%7C%20%5Ccup%7CN%28v%29%7C%7D%7D%0A&id=jYQY8)

其中N(u) , N(v) 分别表示用户u和用户v交互商品的集合。

由于Jaccard相似系数一般无法反映具体用户的评分喜好信息, 所以常用来评估用户是否会对某商品进行打分,而不是预估用户会对某商品打多少分

2.1.2 余弦相似度

余弦相似度衡量了两个向量的夹角,夹角越小越相似。首先从集合的角度描述余弦相似度,相比于Jaccard公式来说就是分母有差异,不是两个用户交互商品的并集的数量,而是两个用户分别交互的商品数量的乘积,公式如下:

第二章 协同过滤(Collaborative Filtering) - 图4%20%5Ccap%20N(v)%7C%7D%7B%5Csqrt%7B%7CN(u)%7C%5Ccdot%7CN(v)%7C%7D%7D%0A#card=math&code=sim_%7Buv%7D%3D%5Cfrac%7B%7CN%28u%29%20%5Ccap%20N%28v%29%7C%7D%7B%5Csqrt%7B%7CN%28u%29%7C%5Ccdot%7CN%28v%29%7C%7D%7D%0A&id=NInmN)

向量的角度进行描述,令矩阵A为用户-商品交互矩阵(因为TopN推荐并不需要用户对物品的评分,只需要知道用户对商品是否有交互就行),即矩阵的每一行表示一个用户对所有商品的交互情况,有交互的商品值为1没有交互的商品值为0,矩阵的列表示所有商品。若用户和商品数量分别为m,n的话,交互矩阵A就是一个m行n列的矩阵。此时用户的相似度可以表示为(其中第二章 协同过滤(Collaborative Filtering) - 图5指的是向量点积):

第二章 协同过滤(Collaborative Filtering) - 图6%20%3D%5Cfrac%7Bu%5Ccdot%20v%7D%7B%7Cu%7C%5Ccdot%20%7Cv%7C%7D%0A#card=math&code=sim_%7Buv%7D%20%3D%20cos%28u%2Cv%29%20%3D%5Cfrac%7Bu%5Ccdot%20v%7D%7B%7Cu%7C%5Ccdot%20%7Cv%7C%7D%0A&id=DBklL)

上述用户-商品交互矩阵在现实情况下是非常的稀疏了,为了避免存储这么大的稀疏矩阵,在计算用户相似度的时候一般会采用集合的方式进行计算。理论上向量之间的相似度计算公式都可以用来计算用户之间的相似度,但是会根据实际的情况选择不同的用户相似度度量方法

可以用cosine_similarity进行实现

  1. from sklearn.metrics.pairwise import cosine_similarity
  2. i = [1, 0, 0, 0]
  3. j = [1, 0.5, 0.5, 0]
  4. cosine_similarity([i, j])
  5. # array([[1. , 0.81649658], [0.81649658, 1. ]])

上式计算出的是一个相似度矩阵,其中对角线表示和自身的相似度

2.1.3 皮尔逊相关系数

皮尔逊相关系数的公式与余弦相似度的计算公式非常的类似,首先对于上述的余弦相似度的计算公式写成求和的形式:

第二章 协同过滤(Collaborative Filtering) - 图7

其中第二章 协同过滤(Collaborative Filtering) - 图8第二章 协同过滤(Collaborative Filtering) - 图9分别表示用户u和用户v对商品i是否有交互(或者具体的评分值)

皮尔逊相关系数计算公式如下:

第二章 协同过滤(Collaborative Filtering) - 图10%3D%5Cfrac%7B%5Csum%7Bi%5Cin%20I%7D(r%7Bui%7D-%5Cbar%20ru)(r%7Bvi%7D-%5Cbar%20rv)%7D%7B%5Csqrt%7B%5Csum%7Bi%5Cin%20I%20%7D(r%7Bui%7D-%5Cbar%20r_u)%5E2%7D%5Csqrt%7B%5Csum%7Bi%5Cin%20I%20%7D(r%7Bvi%7D-%5Cbar%20r_v)%5E2%7D%7D%0A#card=math&code=sim%28u%2Cv%29%3D%5Cfrac%7B%5Csum%7Bi%5Cin%20I%7D%28r%7Bui%7D-%5Cbar%20r_u%29%28r%7Bvi%7D-%5Cbar%20rv%29%7D%7B%5Csqrt%7B%5Csum%7Bi%5Cin%20I%20%7D%28r%7Bui%7D-%5Cbar%20r_u%29%5E2%7D%5Csqrt%7B%5Csum%7Bi%5Cin%20I%20%7D%28r_%7Bvi%7D-%5Cbar%20r_v%29%5E2%7D%7D%0A&id=XWcjO)

其中第二章 协同过滤(Collaborative Filtering) - 图11第二章 协同过滤(Collaborative Filtering) - 图12分别表示分别表示用户u和用户v对商品i是否有交互(或者具体的评分值),第二章 协同过滤(Collaborative Filtering) - 图13分别表示用户第二章 协同过滤(Collaborative Filtering) - 图14和用户第二章 协同过滤(Collaborative Filtering) - 图15交互的所有商品交互数量或者具体评分的平均值。

所以相比余弦相似度,皮尔逊相关系数通过使用用户的平均分对各独立评分进行修正,减小了用户评分偏置的影响

可以用scipy实现计算皮尔逊相关系数

  1. from scipy.stats import pearsonr
  2. i = [1, 0, 0, 0]
  3. j = [1, 0.5, 0.5, 0]
  4. pearsonr(i, j)
  5. # (0.8164965809277258, 0.1835034190722742)

2.2 基于用户的协同过滤

2.2.1 UserCF原理

基于用户的协同过滤,简称UserCF,其工作流程如下图所示

第二章 协同过滤(Collaborative Filtering) - 图16

UserCF的思想其实比较简单,首先是根据目标用户对物品的偏好找到兴趣相近的用户作为相邻用户,然后将相邻用户的喜爱的作品推荐给目标用户;即当一个用户A需要个性化推荐的时候,我们可以先找到和他有相似兴趣的其他用户,然后把那些用户喜欢的,而用户A没有听说过的物品推荐给A。
image-20210719091802451.png

UserCF主要有两个步骤:

  • 找到和目标用户兴趣相似的集合
  • 找到这个集合中的用户喜欢的, 且目标用户没有听说过的物品推荐给目标用户

上面的两个步骤中, 第一个步骤, 我们会基于前面给出的相似性度量的方法找出与目标用户兴趣相似的用户;而在第二个步骤, 如何基于相似用户喜欢的物品来对目标用户进行推荐呢? 这个要依赖于目标用户对相似用户喜欢的物品的一个喜好程度, 如何衡量这个程度大小呢? 为了更好理解上面的两个步骤, 下面通过一个例子把两个步骤具体化。

以下图为例

图片image-20210629232622758.png

给用户推荐物品的过程可以形象化为一个猜测用户对商品进行打分的任务,上面表格里面是5个用户对于5件物品的一个打分情况,就可以理解为用户对物品的喜欢程度

现在根据上图例子描述下UserCF算法的两个步骤:

  • 首先根据前面的这些打分情况(或者说已有的用户向量)计算一下Alice和用户1, 2, 3, 4的相似程度, 找出与Alice最相似的n个用户
  • 根据这n个用户对物品5的评分情况和与Alice的相似程度会猜测出Alice对物品5的评分, 如果评分比较高的话, 就把物品5推荐给用户Alice, 否则不推荐

关于第一个步骤, 前一节已经给出了计算两个用户相似性的方法, 这里不再过多赘述, 这里主要解决第二个问题, 如何产生最终结果的预测

根据上面的几种方法, 我们可以计算出向量之间的相似程度, 也就是可以计算出Alice和其他用户的相近程度, 这时候我们就可以选出与Alice最相近的前n个用户, 基于他们对物品5的评价猜测出Alice的打分值, 那么是怎么计算的呢?

这里常用的方式之一是利用用户相似度和相似用户的评价加权平均获得用户的评价预测, 用下面式子表示:

第二章 协同过滤(Collaborative Filtering) - 图19%7D%7B%5Csum%7B%5Cmathrm%7Bs%7D%20%5Cin%20S%7D%20w%7B%5Cmathrm%7Bu%7D%2C%20%5Cmathrm%7Bs%7D%7D%7D%0A#card=math&code=R%7B%5Cmathrm%7Bu%7D%2C%20%5Cmathrm%7Bp%7D%7D%3D%5Cfrac%7B%5Csum%7B%5Cmathrm%7Bs%7D%20%5Cin%20S%7D%5Cleft%28w%7B%5Cmathrm%7Bu%7D%2C%20%5Cmathrm%7Bs%7D%7D%20%5Ccdot%20R%7B%5Cmathrm%7Bs%7D%2C%20%5Cmathrm%7Bp%7D%7D%5Cright%29%7D%7B%5Csum%7B%5Cmathrm%7Bs%7D%20%5Cin%20S%7D%20w%7B%5Cmathrm%7Bu%7D%2C%20%5Cmathrm%7Bs%7D%7D%7D%0A&id=yP57V)

这个式子里面, 权重第二章 协同过滤(Collaborative Filtering) - 图20是用户u和用户s的相似度,第二章 协同过滤(Collaborative Filtering) - 图21是用户s对物品p的评分。

还有一种方式如下, 这种方式考虑的更加深入, 依然是用户相似度作为权值, 但后面不仅仅是其他用户对物品的评分, 而是该物品的评分与此用户的所有评分的差值进行加权平均, 这时候考虑到了有的用户内心的评分标准不一的情况, 即有的用户喜欢打高分, 有的用户喜欢打低分的情况

第二章 协同过滤(Collaborative Filtering) - 图22%5Cright)%7D%7B%5Csum%7Bk%3D1%7D%5E%7Bn%7D%20S%7Bj%2C%20k%7D%7D%0A#card=math&code=P%7Bi%2C%20j%7D%3D%5Cbar%7BR%7D%7Bi%7D%2B%5Cfrac%7B%5Csum%7Bk%3D1%7D%5E%7Bn%7D%5Cleft%28S%7Bi%2C%20k%7D%5Cleft%28R%7Bk%2C%20j%7D-%5Cbar%7BR%7D%7Bk%7D%5Cright%29%5Cright%29%7D%7B%5Csum%7Bk%3D1%7D%5E%7Bn%7D%20S%7Bj%2C%20k%7D%7D%0A&id=NhWFw)

所以这种计算方法明显更为严谨。

在获得用户u对不同物品的评价预测后, 最终的推荐列表根据预测评分进行排序得到。 至此,基于用户的协同过滤算法的推荐过程完成。下面来计算一下这个过程。

  1. 计算Alice与其他用户的相似度(这里使用皮尔逊相关系数)
    微信截图_20210719114557.png
    这里我们使用皮尔逊相关系数, Alice与user_1的相似度是0.85。同样的方式,可以计算与其他用户的相似度, 这里可以使用numpy的相似度函数得到用户的相似性矩阵
    从这里可以看出,Alice和user_2、user_3、user_4的相似度是0.7、0、 -0.79。 所以,如果n=2, 找到与Alice最相近的两个用户是user_1(相似度:0.85)和user_2(相似度是0.7)。 ```python from sklearn.metrics.pairwise import cosine_similarity import numpy as np

users = np.array([[5, 3, 4, 4], [3, 1, 2, 3], [4, 3, 4, 3], [3, 3, 1, 5], [1, 5, 5, 2]])

皮尔逊相关系数

cosine_similarity(users)

余弦相似性

np.corrcoef(users)

‘’’ 结果: 皮尔逊相关系数矩阵: array([[1. , 0.9753213 , 0.99224264, 0.89072354, 0.79668736], [0.9753213 , 1. , 0.94362852, 0.91160719, 0.67478587], [0.99224264, 0.94362852, 1. , 0.85280287, 0.85811633], [0.89072354, 0.91160719, 0.85280287, 1. , 0.67082039], [0.79668736, 0.67478587, 0.85811633, 0.67082039, 1. ]]) 余弦相关系数矩阵: array([[ 1. , 0.85280287, 0.70710678, 0. , -0.79211803], [ 0.85280287, 1. , 0.30151134, 0.42640143, -0.88662069], [ 0.70710678, 0.30151134, 1. , -0.70710678, -0.14002801], [ 0. , 0.42640143, -0.70710678, 1. , -0.59408853], [-0.79211803, -0.88662069, -0.14002801, -0.59408853, 1. ]]) ‘’’


2.  **根据相似度用户计算Alice对物品5的最终得分**<br />用户1对物品5的评分是3, 用户2对物品5的打分是5, 那么根据上面的计算公式, 可以计算出Alice对物品5的最终得分是 ![](https://g.yuque.com/gr/latex?P_%7BAlice%2C%20%E7%89%A9%E5%93%815%7D%3D%5Cbar%7BR%7D_%7BAlice%7D%2B%5Cfrac%7B%5Csum_%7Bk%3D1%7D%5E%7B2%7D%5Cleft(S_%7BAlice%2Cuser%20k%7D%5Cleft(R_%7Buserk%2C%20%E7%89%A9%E5%93%815%7D-%5Cbar%7BR%7D_%7Buserk%7D%5Cright)%5Cright)%7D%7B%5Csum_%7Bk%3D1%7D%5E%7B2%7D%20S_%7BAlice%2C%20userk%7D%7D%3D4%2B%5Cfrac%7B0.85*(3-2.4)%2B0.7*(5-3.8)%7D%7B0.85%2B0.7%7D%3D4.87%0A#card=math&code=P_%7BAlice%2C%20%E7%89%A9%E5%93%815%7D%3D%5Cbar%7BR%7D_%7BAlice%7D%2B%5Cfrac%7B%5Csum_%7Bk%3D1%7D%5E%7B2%7D%5Cleft%28S_%7BAlice%2Cuser%20k%7D%5Cleft%28R_%7Buserk%2C%20%E7%89%A9%E5%93%815%7D-%5Cbar%7BR%7D_%7Buserk%7D%5Cright%29%5Cright%29%7D%7B%5Csum_%7Bk%3D1%7D%5E%7B2%7D%20S_%7BAlice%2C%20userk%7D%7D%3D4%2B%5Cfrac%7B0.85%2A%283-2.4%29%2B0.7%2A%285-3.8%29%7D%7B0.85%2B0.7%7D%3D4.87%0A&id=eNGXa) 
3.  **根据用户评分对用户进行推荐**<br />这时候, 我们就得到了Alice对物品5的得分是4.87, 根据Alice的打分对物品排个序从大到小:物品1>物品5>物品3=物品4>物品2 这时候,如果要向Alice推荐2款产品的话, 我们就可以推荐物品1和物品5给Alice。 

至此, 基于用户的协同过滤算法原理介绍完毕。

<a name="3cc3151a"></a>
### 2.2.2 UserCF实现

前面介绍了UserCF应共分为三个步骤。那么,它的实现也遵循着三个步骤进行:

1.  首先,建立数据表:<br />**这里采用了字典的方式, 之所以没有用pandas, 是因为上面举得这个例子其实是个例, 在真实情况中,用户对物品的打分情况并不会这么完整,会存在大量的空值, 所以矩阵会很稀疏, 这时候用DataFrame, 会有大量的NaN,故这里用字典的形式存储。** 用两个字典, 第一个字典是物品-用户的评分映射, 键是物品1-5, 用A-E来表示, 每一个值又是一个字典, 表示的是每个用户对该物品的打分。 第二个字典是用户-物品的评分映射, 键是上面的五个用户, 用1-5表示, 值是该用户对每个物品的打分。  
```python
import numpy as np
import pandas as pd

# 定义数据集,采用字典存放数据
def loadData():
    items={'A': {1: 5, 2: 3, 3: 4, 4: 3, 5: 1},
           'B': {1: 3, 2: 1, 3: 3, 4: 3, 5: 5},
           'C': {1: 4, 2: 2, 3: 4, 4: 1, 5: 5},
           'D': {1: 4, 2: 3, 3: 3, 4: 5, 5: 2},
           'E': {2: 3, 3: 5, 4: 4, 5: 1}
          }
    users={1: {'A': 5, 'B': 3, 'C': 4, 'D': 4},
           2: {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3},
           3: {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5},
           4: {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4},
           5: {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1}
          }
    return items,users

items, users = loadData()
item_df = pd.DataFrame(items).T
user_df = pd.DataFrame(users).T
  1. 计算用户相似性矩阵
    这个是一个5×5大小的共现矩阵,行代表每个用户, 列代表每个用户, 值代表用户和用户的相关性。这里的思路是这样的, 因为要求任意一对用户的相关性, 所以需要用双层循环遍历用户-物品评分数据, 当不是同一个用户的时候, 我们要去遍历物品-用户评分数据, 在里面去找这两个用户同时对该物品评过分的数据放入到这两个用户向量中。 因为正常情况下会存在很多的Nan, 即可能用户并没有对某个物品进行评分过, 这样的不能当做用户向量的一部分, 没法计算相似性。代码如下:
    这里的similarity_matrix就是所求的用户相似性矩阵,如下图所示
    image-20210719093708472.png
    有了相似性矩阵,我们就可以得到与Alice最相关的前n个用户。 ```python “””计算用户相似性矩阵””” similarity_matrix = pd.DataFrame(np.zeros((len(users), len(users))), index=[1, 2, 3, 4, 5], columns=[1, 2, 3, 4, 5])

遍历每条用户-物品评分数据

for userID in users: for otheruserId in users: vec_user = [] vec_otheruser = [] if userID != otheruserId: for itemId in items: # 遍历物品-用户评分数据 itemRatings = items[itemId] # 这也是个字典 每条数据为所有用户对当前物品的评分 if userID in itemRatings and otheruserId in itemRatings: # 说明两个用户都对该物品评过分 vec_user.append(itemRatings[userID]) vec_otheruser.append(itemRatings[otheruserId])

        # 这里可以获得相似性矩阵(共现矩阵)
        similarity_matrix[userID][otheruserId] = np.corrcoef(np.array(vec_user), np.array(vec_otheruser))[0][1]
        #similarity_matrix[userID][otheruserId] = cosine_similarity(np.array(vec_user), np.array(vec_otheruser))[0][1]

similarity_matrix


3.  计算前n个相似的用户  
```python
"""计算前n个相似的用户"""
n = 2
similarity_users = similarity_matrix[1].sort_values(ascending=False)[:n].index.tolist()    
# [2, 3]   也就是用户1和用户2
  1. 计算最终得分
    结果如下图所示
    image-20210719093935024.png
    """计算最终得分"""
    base_score = np.mean(np.array([value for value in users[1].values()]))
    weighted_scores = 0.
    corr_values_sum = 0.
    for user in similarity_users:  # [2, 3]
    corr_value = similarity_matrix[1][user]            # 两个用户之间的相似性
    mean_user_score = np.mean(np.array([value for value in users[user].values()]))    # 每个用户的打分平均值
    weighted_scores += corr_value * (users[user]['E']-mean_user_score)      # 加权分数
    corr_values_sum += corr_value
    final_scores = base_score + weighted_scores / corr_values_sum
    print('用户Alice对物品5的打分: ', final_scores)
    user_df.loc[1]['E'] = final_scores
    user_df
    

以上就是UserCF的简单实现,完整实现在第2.7节。

2.2.3 UserCF评价

基于用户的协同过滤算法存在两个问题:

  • 数据稀疏性。一个大型的电子商务推荐系统一般有非常多的物品,用户可能买的其中不到1%的物品,不同用户之间买的物品重叠性较低,导致算法无法找到一个用户的邻居,即偏好相似的用户。这导致UserCF不适用于那些正反馈获取较困难的应用场景(如酒店预订,大件商品购买等低频应用)
  • 算法扩展性。基于用户的协同过滤需要维护用户相似度矩阵以便快速的找出Topn相似用户,该矩阵的存储开销非常大,存储空间随着用户数量的增加而增加,不适合用户数据量大的情况使用

由于UserCF技术上的两点缺陷,导致很多电商平台并没有采用这种算法,而是采用了基于物品的协同过滤实现最初的推荐系统。

2.3 基于物品的协同过滤

2.3.1 ItemCF原理

基于物品的协同过滤, 简称ItemCF,其工作流程如下图所示。

第二章 协同过滤(Collaborative Filtering) - 图26

基本思想是预先根据所有用户的历史偏好数据计算物品之间的相似性,然后把与用户喜欢的物品相类似的物品推荐给用户。比如物品a和c非常相似,因为喜欢a的用户同时也喜欢c,而用户A喜欢a,所以把c推荐给用户A。ItemCF算法并不利用物品的内容属性计算物品之间的相似度,主要通过分析用户的行为记录计算物品之间的相似度,该算法认为,物品a和物品c具有很大的相似度是因为喜欢物品a的用户大都喜欢物品c

image-20210719114710482.png

2.3.2 ItemCF原理与实现

基于物品的协同过滤算法主要分为两步

  • 计算物品之间的相似度
  • 根据物品的相似度和用户的历史行为给用户生成推荐列表(购买了该商品的用户也经常购买的其他商品)

基于物品的协同过滤算法和基于用户的协同过滤算法很像, 仍然使用上一节Alice的那个例子。

第二章 协同过滤(Collaborative Filtering) - 图28

如果想知道Alice对物品5打多少分,基于物品的协同过滤算法的步骤如下:

  • 首先计算一下物品5和物品1, 2, 3, 4之间的相似性(它们也是向量的形式, 每一列的值就是它们的向量表示, 因为ItemCF认为物品a和物品c具有很大的相似度是因为喜欢物品a的用户大都喜欢物品c, 所以就可以基于每个用户对该物品的打分或者说喜欢程度来向量化物品)
  • 找出与物品5最相近的n个物品
  • 根据Alice对最相近的n个物品的打分去计算对物品5的打分情况

下面来计算一下:

  • 首先是计算物品的相似性矩阵:
    image-20210719114734392.png
  • 根据皮尔逊相关系数, 可以找到与物品5最相似的2个物品(n=2)是item1和item4,下面基于上面的公式计算最终得分: 第二章 协同过滤(Collaborative Filtering) - 图30%5Cright)%7D%7B%5Csum%7Bk%3D1%7D%5E%7B2%7D%20S%7B%E7%89%A9%E5%93%81k%2C%20%E7%89%A9%E5%93%815%7D%7D%3D%5Cfrac%7B13%7D%7B4%7D%2B%5Cfrac%7B0.97(5-3.2)%2B0.58(4-3.4)%7D%7B0.97%2B0.58%7D%3D4.6%0A#card=math&code=P%7BAlice%2C%20%E7%89%A9%E5%93%815%7D%3D%5Cbar%7BR%7D%7B%E7%89%A9%E5%93%815%7D%2B%5Cfrac%7B%5Csum%7Bk%3D1%7D%5E%7B2%7D%5Cleft%28S%7B%E7%89%A9%E5%93%815%2C%E7%89%A9%E5%93%81%20k%7D%5Cleft%28R%7BAlice%2C%20%E7%89%A9%E5%93%81k%7D-%5Cbar%7BR%7D%7B%E7%89%A9%E5%93%81k%7D%5Cright%29%5Cright%29%7D%7B%5Csum%7Bk%3D1%7D%5E%7B2%7D%20S%7B%E7%89%A9%E5%93%81k%2C%20%E7%89%A9%E5%93%815%7D%7D%3D%5Cfrac%7B13%7D%7B4%7D%2B%5Cfrac%7B0.97%2A%285-3.2%29%2B0.58%2A%284-3.4%29%7D%7B0.97%2B0.58%7D%3D4.6%0A&id=CaRwC)
    【注意】:由于这里对角线元素是1,会导致和物品最相似的物品是其本身,所以需要进行处理,这里我直接更改代码吧最相似的去掉 ```python

    定义数据集,采用字典存放数据

    def loadData(): items={‘A’: {1: 5, 2: 3, 3: 4, 4: 3, 5: 1},
        'B': {1: 3, 2: 1, 3: 3, 4: 3, 5: 5},
        'C': {1: 4, 2: 2, 3: 4, 4: 1, 5: 5},
        'D': {1: 4, 2: 3, 3: 3, 4: 5, 5: 2},
        'E': {2: 3, 3: 5, 4: 4, 5: 1}
       }
    
    users={1: {‘A’: 5, ‘B’: 3, ‘C’: 4, ‘D’: 4},
        2: {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3},
        3: {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5},
        4: {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4},
        5: {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1}
       }
    
    return items,users

items, users = loadData()

“””计算物品的相似矩阵””” similarity_matrix = pd.DataFrame(np.ones((len(items), len(items))), index=[‘A’, ‘B’, ‘C’, ‘D’, ‘E’], columns=[‘A’, ‘B’, ‘C’, ‘D’, ‘E’])

遍历每条物品-用户评分数据

for itemId in items: for otheritemId in items: vec_item = [] # 定义列表, 保存当前两个物品的向量值 vec_otheritem = []

    #userRagingPairCount = 0     # 两件物品均评过分的用户数
    if itemId != otheritemId:    # 物品不同
        for userId in users:    # 遍历用户-物品评分数据
            userRatings = users[userId]    # 每条数据为该用户对所有物品的评分, 这也是个字典

            if itemId in userRatings and otheritemId in userRatings:   # 用户对这两个物品都评过分
                #userRagingPairCount += 1
                vec_item.append(userRatings[itemId])
                vec_otheritem.append(userRatings[otheritemId])

        # 这里可以获得相似性矩阵(共现矩阵)
        similarity_matrix[itemId][otheritemId] = np.corrcoef(np.array(vec_item), np.array(vec_otheritem))[0][1]
        # similarity_matrix[itemId][otheritemId] = cosine_similarity(np.array(vec_item), np.array(vec_otheritem))[0][1]

similarity_matrix


-  最后也是得到与物品5相似的前n个物品, 计算出最终得分。 
```python
"""得到与物品5相似的前n个物品"""
n = 2
# similarity_items = similarity_matrix['E'].sort_values(ascending=False)[:n].index.tolist()
similarity_items = similarity_matrix['E'].sort_values(ascending=False)[1:n+1].index.tolist()  
# ['A', 'D']

"""计算最终得分"""
base_score = np.mean(np.array([value for value in items['E'].values()]))
weighted_scores = 0.
corr_values_sum = 0.
for item in similarity_items:  # ['A', 'D']
    corr_value = similarity_matrix['E'][item]            # 两个物品之间的相似性
    mean_item_score = np.mean(np.array([value for value in items[item].values()]))    # 每个物品的打分平均值
    weighted_scores += corr_value * (users[1][item]-mean_item_score)      # 加权分数
    corr_values_sum += corr_value
final_scores = base_score + weighted_scores / corr_values_sum
print('用户Alice对物品5的打分: ', final_scores)
user_df.loc[1]['E'] = final_scores
user_df

以上就是ItemCF的简单实现,完整的实现在第2.7节

2.4 算法评估

由于UserCF和ItemCF结果评估采用统一的标识,所以统一在这一节介绍。

2.4.1 召回率

对用户u推荐N个物品记为R(u), 令用户u在测试集上喜欢的物品集合为T(u), 那么召回率定义为:

第二章 协同过滤(Collaborative Filtering) - 图31%20%5Ccap%20T(u)%7C%7D%7B%5Csum%7Bu%7D%7CT(u)%7C%7D%0A#card=math&code=%5Coperatorname%7BRecall%7D%3D%5Cfrac%7B%5Csum%7Bu%7D%7CR%28u%29%20%5Ccap%20T%28u%29%7C%7D%7B%5Csum_%7Bu%7D%7CT%28u%29%7C%7D%0A&id=CdCG8)

这个意思就是说,在用户真实购买或者看过的影片里面,模型真正预测出了多少,这个考察的是模型推荐的一个全面性

2.4.2 准确率

准确率定义为:

第二章 协同过滤(Collaborative Filtering) - 图32%20%5Ccap%20T(u)%7C%7D%7B%5Csum%7Bu%7D%7CR(u)%7C%7D%0A#card=math&code=%5Coperatorname%7BPrecision%7D%3D%5Cfrac%7B%5Csum%7Bu%7D%20%5Cmid%20R%28u%29%20%5Ccap%20T%28u%29%7C%7D%7B%5Csum_%7Bu%7D%7CR%28u%29%7C%7D%0A&id=JRwxw)

这个意思是,在推荐的所有物品中,用户真正看的有多少,这个考察的是模型推荐的一个准确性

为了提高准确率,模型需要把非常有把握的才对用户进行推荐,所以这时候就减少了推荐的数量,而这往往就损失了全面性,真正预测出来的会非常少,所以实际应用中应该综合考虑两者的平衡。

2.4.3 覆盖率

覆盖率反映了推荐算法发掘长尾的能力,覆盖率越高,说明推荐算法越能将长尾中的物品推荐给用户

第二章 协同过滤(Collaborative Filtering) - 图33%5Cright%7C%7D%7B%7CI%7C%7D%0A#card=math&code=%5Ctext%20%7B%20Coverage%20%7D%3D%5Cfrac%7B%5Cleft%7C%5Cbigcup_%7Bu%20%5Cin%20U%7D%20R%28u%29%5Cright%7C%7D%7B%7CI%7C%7D%0A&id=T3gt4)

该覆盖率表示最终的推荐列表中包含多大比例的物品。如果所有物品都被给推荐给至少一个用户,那么覆盖率是100%。

2.4.4 新颖度

推荐列表中物品的平均流行度度量推荐结果的新颖度。如果推荐出的物品都很热门,说明推荐的新颖度较低。由于物品的流行度分布呈长尾分布,所以为了流行度的平均值更加稳定, 在计算平均流行度时对每个物品的流行度取对数。

2.5 协同过滤算法的权重改进

第二章 协同过滤(Collaborative Filtering) - 图34

  1. 基础算法
    图1为最简单的计算物品相关度的公式,分子为同时喜好第二章 协同过滤(Collaborative Filtering) - 图35第二章 协同过滤(Collaborative Filtering) - 图36的用户数
  2. 热门物品的惩罚
    图1存在一个问题,如果 item-j 是很热门的商品,导致很多喜欢第二章 协同过滤(Collaborative Filtering) - 图37的用户都喜欢第二章 协同过滤(Collaborative Filtering) - 图38,这时第二章 协同过滤(Collaborative Filtering) - 图39就会非常大。同样,几乎所有的物品都和 第二章 协同过滤(Collaborative Filtering) - 图40 的相关度非常高,这显然是不合理的。所以图2中分母通过引入第二章 协同过滤(Collaborative Filtering) - 图41#card=math&code=N%28j%29&id=YqEtw)来对 第二章 协同过滤(Collaborative Filtering) - 图42 的热度进行惩罚。如果物品很热门,那么N(j)就会越大,对应的权重就会变小。
  3. 热门物品的进一步惩罚
    如果 第二章 协同过滤(Collaborative Filtering) - 图43 极度热门,上面的算法还是不够的。举个例子,《Harry Potter》非常火,买任何一本书的人都会购买它,即使通过图2的方法对它进行了惩罚,但是《Harry Potter》仍然会获得很高的相似度。这就是推荐系统领域著名的 Harry Potter Problem。
    如果需要进一步对热门物品惩罚,可以继续修改公式为如图3所示,通过调节参数 α,α 越大,惩罚力度越大,热门物品的相似度越低,整体结果的平均热门程度越低。
  4. 活跃用户的惩罚
    同样的,Item-based CF也需要考虑活跃用户(即一个活跃用户(专门做刷单)可能买了非常多的物品)的影响,活跃用户对物品相似度的贡献应该小于不活跃用户。图4为集合了该权重的算法。

2.6 协同过滤算法存在的问题

协同过滤推荐算法给我们带来便利的同时,也存在一些问题,在实际应用过程中我们经常会遇到,以下就存在的一些典型的问题进行了分析。

2.6.1 数据稀疏性问题

推荐系统面临的问题中最大的问题就是数据稀疏性问题,它会影响推荐系统推荐的准确度,在个性化推荐系统中,协同过滤是最常用的技术,数据稀疏性影响了协同过滤的结果,因为在很多推荐系统中只有很小一部分评分,当在评分很小的情况下,推荐的准确度也会下降。

目前来说解决数据稀疏问题最好的算法是降维技术,比如奇异值分解(SVD)。降维技术的关键是通过原始矩阵通过计算来求其低维度的相似来降低矩阵的维度,有一点弊端就是数据量大的时候运算成本很高,好在最近几年云计算发展迅猛,这些计算量都可以放在云端,来弥补计算量的问题,除了降维技术之外人口统计过滤也是解决数据稀疏性问题的很好的方法,人口统计过滤是通过用户资料的对比找到用户相似,然后通过相似的用户来进行推荐,这种方式可以解决数据量很稀疏的情况下使用。

2.6.2 冷启动问题

冷启动问题也被认作为推荐系统面临的问题中关键之一。冷启动产生原因是新用户进入系统没有用户行为数据,所以就不存在收集用户偏好的过程,进而也就很难进行有效的推荐。解决用户冷启动的问题可以采用上一小节中的人口统计过滤,其针对于冷启动也会有很好的效果,除此之外只能通过业务上调整去改变并获得用户的一些资料,从而进行有效推荐,具体的业务有利用用户注册信息、社交网络挖掘、启用用户信息。

具体做法是通过用户信息进行用户分类,利用社交网络导入用户的好友关系,然后通过好友关系来进行相关推荐,还可以通过用户的首次访问不立即给推荐反馈结果,而是通过引导页用户填写用户感兴趣的内容,然后通过这些收集到的准确信息来进行准确的推荐,一般我们会通过引导用户填写比较有代表性、区分性以及多样性的内容去进行选择,比如在豆瓣FM中,一般会采用热门精选的音乐以及分组好的音乐来给用户进行选择。解决内容冷启动问题可以通过物品的信息来解决新物品的推荐问题,一般会采用物品标签进行推荐,通过抽象出新物品的标签以及利用老物品的标签进行匹配,匹配成功就进行相关度的推荐。而系统的冷启动可以通过专家发挥专家的作用,在系统冷启动环节中,可以引入专家的知识,利用专家的知识构建起物品的相关度表,然后通过表格进行相关度的分析进行推荐。

2.6.3 扩展性问题

推荐系统中通过推荐算法获得之后,因为用户之前没有足够多的对物品进行评分,有时候很难掌握用户的各方面兴趣和需求,这就会导致出现过拟合现象,这一类现象就是推荐系统中的扩展性问题,这类问题对于推荐系统的体验上是一个很大的问题。

扩展性问题在实际应用中是无可避免的,其中受数据集大小的扩展,数据完整性等问题的限制,对于数据完整性的问题,通常是引入随机性,使算法收敛到全局最优或者逼近全局最优,但是推荐系统运行过程是动态变化的,同时也会受冷启动影响,用户/对象/喜好数据达到一定规模时,性能这个时候也会存在一些瓶颈,这个时候的大数据处理和算法的可扩展性成为推荐算法技术需要实施并迫切需要解决的问题。

2.6.4 泛化能力弱的问题

协同过滤算法存在的问题之一就是泛化能力弱, 即协同过滤无法将两个物品相似的信息推广到其他物品的相似性上。 导致的问题是热门物品具有很强的头部效应, 容易跟大量物品产生相似, 而尾部物品由于特征向量稀疏, 导致很少被推荐。 比如下面这个例子:

第二章 协同过滤(Collaborative Filtering) - 图44

A, B, C, D是物品, 看右边的物品共现矩阵, 可以发现物品D与A、B、C的相似度比较大, 所以很有可能将D推荐给用过A、B、C的用户。 但是物品D与其他物品相似的原因是因为D是一件热门商品, 系统无法找出A、B、C之间相似性的原因是其特征太稀疏, 缺乏相似性计算的直接数据。 所以这就是协同过滤的天然缺陷:推荐系统头部效应明显, 处理稀疏向量的能力弱。

为了解决这个问题,同时增加模型的泛化能力,2006年,矩阵分解技术(Matrix Factorization,MF) 被提出,该方法在协同过滤共现矩阵的基础上,使用更稠密的隐向量表示用户和物品,挖掘用户和物品的隐含兴趣和隐含特征,在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。

2.6.5 其他的一些问题和挑战

推荐系统在推荐的过程中还会存在隐私问题、噪声问题、解释性问题、合理性、新颖性等等,这些问题在推荐系统中很有挑战性,但是解决这些问题出现的同时也会改变推荐准确度,所以我们需要权衡这些问题和推荐之间的联系,目前很多算法都有在改进相关的问题,改进的措施也是多维度的。

2.7 协同过滤实战:电影推荐

本节通过使用movielens数据集进行userCF和itemCF的实战

2.7.1 userCF

import pandas as pd
import numpy as np
import warnings
import random, math, os
from tqdm import tqdm
from sklearn.model_selection import train_test_split
warnings.filterwarnings('ignore')

# 评价指标
# 推荐系统推荐正确的商品数量占用户实际点击的商品数量
def Recall(Rec_dict, Val_dict):
    '''
    Rec_dict: 推荐算法返回的推荐列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...} 
    Val_dict: 用户实际点击的商品列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...}
    '''
    hit_items = 0
    all_items = 0
    for uid, items in Val_dict.items():
        rel_set = items
        rec_set = Rec_dict[uid]
        for item in rec_set:
            if item in rel_set:
                hit_items += 1
        all_items += len(rel_set)

    return round(hit_items / all_items * 100, 2)

# 推荐系统推荐正确的商品数量占给用户实际推荐的商品数
def Precision(Rec_dict, Val_dict):
    '''
    Rec_dict: 推荐算法返回的推荐列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...} 
    Val_dict: 用户实际点击的商品列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...}
    '''
    hit_items = 0
    all_items = 0
    for uid, items in Val_dict.items():
        rel_set = items
        rec_set = Rec_dict[uid]
        for item in rec_set:
            if item in rel_set:
                hit_items += 1
        all_items += len(rec_set)

    return round(hit_items / all_items * 100, 2)

# 所有被推荐的用户中,推荐的商品数量占这些用户实际被点击的商品数量
def Coverage(Rec_dict, Trn_dict):
    '''
    Rec_dict: 推荐算法返回的推荐列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...} 
    Trn_dict: 训练集用户实际点击的商品列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...}
    '''
    rec_items = set()
    all_items = set()
    for uid in Rec_dict:
        for item in Trn_dict[uid]:
            all_items.add(item)
        for item in Rec_dict[uid]:
            rec_items.add(item)
    return round(len(rec_items) / len(all_items) * 100, 2)

# 使用平均流行度度量新颖度,如果平均流行度很高(即推荐的商品比较热门),说明推荐的新颖度比较低
def Popularity(Rec_dict, Trn_dict):
    '''
    Rec_dict: 推荐算法返回的推荐列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...} 
    Trn_dict: 训练集用户实际点击的商品列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...}
    '''
    pop_items = {}
    for uid in Trn_dict:
        for item in Trn_dict[uid]:
            if item not in pop_items:
                pop_items[item] = 0
            pop_items[item] += 1

    pop, num = 0, 0
    for uid in Rec_dict:
        for item in Rec_dict[uid]:
            pop += math.log(pop_items[item] + 1) # 物品流行度分布满足长尾分布,取对数可以使得平均值更稳定
            num += 1  
    return round(pop / num, 3)

# 将几个评价指标指标函数一起调用
def rec_eval(val_rec_items, val_user_items, trn_user_items):
    print('recall:', Recall(val_rec_items, val_user_items))
    print('precision', Precision(val_rec_items, val_user_items))
    print('coverage', Coverage(val_rec_items, trn_user_items))
    print('Popularity', Popularity(val_rec_items, trn_user_items))

def get_data(root_path):
    # 读取数据
    rnames = ['user_id', 'movie_id', 'rating', 'timestamp']
    ratings = pd.read_csv(os.path.join(root_path, 'ratings.dat'), sep='::', engine='python', names=rnames)

    # 分割训练和验证集
    trn_data, val_data, _, _ = train_test_split(ratings, ratings, test_size=0.2)

    trn_data = trn_data.groupby('user_id')['movie_id'].apply(list).reset_index()
    val_data = val_data.groupby('user_id')['movie_id'].apply(list).reset_index()

    trn_user_items = {}
    val_user_items = {}

    # 将数组构造成字典的形式{user_id: [item_id1, item_id2,...,item_idn]}
    for user, movies in zip(*(list(trn_data['user_id']), list(trn_data['movie_id']))):
        trn_user_items[user] = set(movies)

    for user, movies in zip(*(list(val_data['user_id']), list(val_data['movie_id']))):
        val_user_items[user] = set(movies)

    return trn_user_items, val_user_items

def User_CF_Rec(trn_user_items, val_user_items, K, N):
    '''
    trn_user_items: 表示训练数据,格式为:{user_id1: [item_id1, item_id2,...,item_idn], user_id2...}
    val_user_items: 表示验证数据,格式为:{user_id1: [item_id1, item_id2,...,item_idn], user_id2...}
    K: K表示的是相似用户的数量,每个用户都选择与其最相似的K个用户
    N: N表示的是给用户推荐的商品数量,给每个用户推荐相似度最大的N个商品
    '''

    # 建立item->users倒排表
    # 倒排表的格式为: {item_id1: {user_id1, user_id2, ... , user_idn}, item_id2: ...} 也就是每个item对应有那些用户有过点击
    # 建立倒排表的目的就是为了更好的统计用户之间共同交互的商品数量
    print('建立倒排表...')
    item_users = {}
    for uid, items in tqdm(trn_user_items.items()): # 遍历每一个用户的数据,其中包含了该用户所有交互的item
        for item in items: # 遍历该用户的所有item, 给这些item对应的用户列表添加对应的uid
            if item not in item_users:
                item_users[item] = set()
            item_users[item].add(uid)


    # 计算用户协同过滤矩阵
    # 即利用item-users倒排表统计用户之间交互的商品数量,用户协同过滤矩阵的表示形式为:sim = {user_id1: {user_id2: num1}, user_id3:{user_id4: num2}, ...}
    # 协同过滤矩阵是一个双层的字典,用来表示用户之间共同交互的商品数量
    # 在计算用户协同过滤矩阵的同时还需要记录每个用户所交互的商品数量,其表示形式为: num = {user_id1:num1, user_id2:num2, ...}
    sim = {}
    num = {}
    print('构建协同过滤矩阵...')
    for item, users in tqdm(item_users.items()): # 遍历所有的item去统计,用户两辆之间共同交互的item数量
        for u in users:
            if u not in num: # 如果用户u不在字典num中,提前给其在字典中初始化为0,否则后面的运算会报key error
                num[u] = 0
            num[u] += 1 # 统计每一个用户,交互的总的item的数量
            if u not in sim: # 如果用户u不在字典sim中,提前给其在字典中初始化为一个新的字典,否则后面的运算会报key error
                sim[u] = {}
            for v in users:
                if u != v:  # 只有当u不等于v的时候才计算用户之间的相似度 
                    if v not in sim[u]:
                        sim[u][v] = 0
                    sim[u][v] += 1


    # 计算用户相似度矩阵
    # 用户协同过滤矩阵其实相当于是余弦相似度的分子部分,还需要除以分母,即两个用户分别交互的item数量的乘积
    # 两个用户分别交互的item数量的乘积就是上面统计的num字典
    print('计算相似度...')
    for u, users in tqdm(sim.items()):
        for v, score in users.items():
            sim[u][v] = score / math.sqrt(num[u] * num[v])  # 余弦相似度分母部分


    # 对验证数据中的每个用户进行TopN推荐
    # 在对用户进行推荐之前需要先通过相似度矩阵得到与当前用户最相思的前K个用户,
    # 然后对这K个用户交互的商品中除当前测试用户训练集中交互过的商品以外的商品计算最终的相似度分数
    # 最终推荐的候选商品的相似度分数是由多个用户对该商品分数的一个累加和
    print('给测试用户进行推荐...')
    items_rank = {}
    for u, _ in tqdm(val_user_items.items()):  # 遍历测试集用户,给测试集中的每个用户进行推荐
        items_rank[u] = {}  # 初始化用户u的候选item的字典
        for v, score in sorted(sim[u].items(), key=lambda x: x[1], reverse=True)[:K]: # 选择与用户u最相思的k个用户
            for item in trn_user_items[v]:  # 遍历相似用户之间交互过的商品
                if item not in trn_user_items[u]:  # 如果相似用户交互过的商品,测试用户在训练集中出现过,就不用进行推荐,直接跳过
                    if item not in items_rank[u]:
                        items_rank[u][item] = 0   # 初始化用户u对item的相似度分数为0
                    items_rank[u][item] += score  # 累加所有相似用户对同一个item的分数

    print('为每个用户筛选出相似度分数最高的N个商品...')
    items_rank = {k: sorted(v.items(), key=lambda x: x[1], reverse=True)[:N] for k, v in items_rank.items()}
    items_rank = {k: set([x[0] for x in v]) for k, v in items_rank.items()}  # 将输出整合成合适的格式输出

    return items_rank

if __name__ == "__main__":
    root_path = './data/ml-1m/'
    trn_user_items, val_user_items = get_data(root_path)
    rec_items = User_CF_Rec(trn_user_items, val_user_items, 80, 10)
    rec_eval(rec_items, val_user_items, trn_user_items)

2.7.2 itemCF

import pandas as pd
import numpy as np
import warnings
import random, math, os
from tqdm import tqdm
from sklearn.model_selection import train_test_split
warnings.filterwarnings('ignore')

# 评价指标
# 推荐系统推荐正确的商品数量占用户实际点击的商品数量
def Recall(Rec_dict, Val_dict):
    '''
    Rec_dict: 推荐算法返回的推荐列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...} 
    Val_dict: 用户实际点击的商品列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...}
    '''
    hit_items = 0
    all_items = 0
    for uid, items in Val_dict.items():
        rel_set = items
        rec_set = Rec_dict[uid]
        for item in rec_set:
            if item in rel_set:
                hit_items += 1
        all_items += len(rel_set)

    return round(hit_items / all_items * 100, 2)

# 推荐系统推荐正确的商品数量占给用户实际推荐的商品数
def Precision(Rec_dict, Val_dict):
    '''
    Rec_dict: 推荐算法返回的推荐列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...} 
    Val_dict: 用户实际点击的商品列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...}
    '''
    hit_items = 0
    all_items = 0
    for uid, items in Val_dict.items():
        rel_set = items
        rec_set = Rec_dict[uid]
        for item in rec_set:
            if item in rel_set:
                hit_items += 1
        all_items += len(rec_set)

    return round(hit_items / all_items * 100, 2)

# 所有被推荐的用户中,推荐的商品数量占这些用户实际被点击的商品数量
def Coverage(Rec_dict, Trn_dict):
    '''
    Rec_dict: 推荐算法返回的推荐列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...} 
    Trn_dict: 训练集用户实际点击的商品列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...}
    '''
    rec_items = set()
    all_items = set()
    for uid in Rec_dict:
        for item in Trn_dict[uid]:
            all_items.add(item)
        for item in Rec_dict[uid]:
            rec_items.add(item)
    return round(len(rec_items) / len(all_items) * 100, 2)

# 使用平均流行度度量新颖度,如果平均流行度很高(即推荐的商品比较热门),说明推荐的新颖度比较低
def Popularity(Rec_dict, Trn_dict):
    '''
    Rec_dict: 推荐算法返回的推荐列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...} 
    Trn_dict: 训练集用户实际点击的商品列表, 形式:{uid: {item1, item2,...}, uid: {item1, item2,...}, ...}
    '''
    pop_items = {}
    for uid in Trn_dict:
        for item in Trn_dict[uid]:
            if item not in pop_items:
                pop_items[item] = 0
            pop_items[item] += 1

    pop, num = 0, 0
    for uid in Rec_dict:
        for item in Rec_dict[uid]:
            pop += math.log(pop_items[item] + 1)  # 物品流行度分布满足长尾分布,取对数可以使得平均值更稳定
            num += 1  
    return round(pop / num, 3)

# 将几个评价指标指标函数一起调用
def rec_eval(val_rec_items, val_user_items, trn_user_items):
    print('recall:', Recall(val_rec_items, val_user_items))
    print('precision', Precision(val_rec_items, val_user_items))
    print('coverage', Coverage(val_rec_items, trn_user_items))
    print('Popularity', Popularity(val_rec_items, trn_user_items))

def get_data(root_path):
    # 读取数据
    rnames = ['user_id','movie_id','rating','timestamp']
    ratings = pd.read_csv(os.path.join(root_path, 'ratings.dat'), sep='::', engine='python', names=rnames)

    # 分割训练和验证集
    trn_data, val_data, _, _ = train_test_split(ratings, ratings, test_size=0.2)

    trn_data = trn_data.groupby('user_id')['movie_id'].apply(list).reset_index()
    val_data = val_data.groupby('user_id')['movie_id'].apply(list).reset_index()

    trn_user_items = {}
    val_user_items = {}

    # 将数组构造成字典的形式{user_id: [item_id1, item_id2,...,item_idn]}
    for user, movies in zip(*(list(trn_data['user_id']), list(trn_data['movie_id']))):
        trn_user_items[user] = set(movies)

    for user, movies in zip(*(list(val_data['user_id']), list(val_data['movie_id']))):
        val_user_items[user] = set(movies)

    return trn_user_items, val_user_items

def Item_CF(trn_user_items, val_user_items, K, N):
    '''
    trn_user_items: 表示训练数据,格式为:{user_id1: [item_id1, item_id2,...,item_idn], user_id2...}
    val_user_items: 表示验证数据,格式为:{user_id1: [item_id1, item_id2,...,item_idn], user_id2...}
    K: K表示的是相似商品的数量,为每个用户交互的每个商品都选择其最相思的K个商品
    N: N表示的是给用户推荐的商品数量,给每个用户推荐相似度最大的N个商品
    '''

    # 建立user->item的倒排表
    # 倒排表的格式为: {user_id1: [item_id1, item_id2,...,item_idn], user_id2: ...} 也就是每个用户交互过的所有商品集合
    # 由于输入的训练数据trn_user_items,本身就是这中格式的,所以这里不需要进行额外的计算


    # 计算商品协同过滤矩阵
    # 即利用user-items倒排表统计商品与商品之间被共同的用户交互的次数
    # 商品协同过滤矩阵的表示形式为:sim = {item_id1: {item_id2: num1}, item_id3: {item_id4: num2}, ...}
    # 商品协同过滤矩阵是一个双层的字典,用来表示商品之间共同交互的用户数量
    # 在计算商品协同过滤矩阵的同时还需要记录每个商品被多少不同用户交互的次数,其表示形式为: num = {item_id1:num1, item_id2:num2, ...}
    sim = {}
    num = {}
    print('构建相似性矩阵...')
    for uid, items in tqdm(trn_user_items.items()):
        for i in items:    
            if i not in num:
                num[i] = 0
            num[i] += 1
            if i not in sim:
                sim[i] = {}
            for j in items:
                if j not in sim[i]:
                    sim[i][j] = 0
                if i != j:
                    sim[i][j] += 1

    # 计算物品的相似度矩阵
    # 商品协同过滤矩阵其实相当于是余弦相似度的分子部分,还需要除以分母,即两个商品被交互的用户数量的乘积
    # 两个商品被交互的用户数量就是上面统计的num字典
    print('计算协同过滤矩阵...')
    for i, items in tqdm(sim.items()):
        for j, score in items.items():
            if i != j:
                sim[i][j] = score / math.sqrt(num[i] * num[j])


    # 对验证数据中的每个用户进行TopN推荐
    # 在对用户进行推荐之前需要先通过商品相似度矩阵得到当前用户交互过的商品最相思的前K个商品,
    # 然后对这K个用户交互的商品中除当前测试用户训练集中交互过的商品以外的商品计算最终的相似度分数
    # 最终推荐的候选商品的相似度分数是由多个相似商品对该商品分数的一个累加和
    items_rank = {}
    print('给用户进行推荐...')
    for uid, _ in tqdm(val_user_items.items()):
        items_rank[uid] = {}  # 存储用户候选的推荐商品
        for hist_item in trn_user_items[uid]:  # 遍历该用户历史喜欢的商品,用来下面寻找其相似的商品
            for item, score in sorted(sim[hist_item].items(), key=lambda x: x[1], reverse=True)[:K]:
                if item not in trn_user_items[uid]:  # 进行推荐的商品一定不能在历史喜欢商品中出现
                    if item not in items_rank[uid]:
                        items_rank[uid][item] = 0
                    items_rank[uid][item] += score

    print('为每个用户筛选出相似度分数最高的N个商品...')
    items_rank = {k: sorted(v.items(), key=lambda x: x[1], reverse=True)[:N] for k, v in items_rank.items()}
    items_rank = {k: set([x[0] for x in v]) for k, v in items_rank.items()}
    return items_rank

if __name__ == "__main__":
    root_path = './data/ml-1m/'
    trn_user_items, val_user_items = get_data(root_path)
    rec_items = Item_CF(trn_user_items, val_user_items, 80, 10)
    rec_eval(rec_items, val_user_items, trn_user_items)

2.8 课后问题

  1. 什么时候使用UserCF,什么时候使用ItemCF?为什么?
    1)UserCF 由于是基于用户相似度进行推荐, 所以具备更强的社交特性, 这样的特点非常适于用户少, 物品多, 时效性较强的场合, 比如新闻推荐场景, 因为新闻本身兴趣点分散, 相比用户对不同新闻的兴趣偏好, 新闻的及时性,热点性往往更加重要, 所以正好适用于发现热点,跟踪热点的趋势。 另外还具有推荐新信息的能力, 更有可能发现惊喜,因为看的是人与人的相似性,推出来的结果可能更有惊喜,可以发现用户潜在但自己尚未察觉的兴趣爱好。对于用户较少, 要求时效性较强的场合, 就可以考虑UserCF
    2)ItemCF 这个更适用于兴趣变化较为稳定的应用, 更接近于个性化的推荐, 适合物品少,用户多,用户兴趣固定持久, 物品更新速度不是太快的场合, 比如推荐艺术品,音乐,电影。
  2. 协同过滤在计算上有什么缺点?有什么比较好的思路可以解决(缓解)
    协同过滤在计算上的缺点是它具有较差的稀疏向量处理能力
    第一个问题就是泛化能力弱,即协同过滤无法将两个物品相似的信息推广到其他物品的相似性上。导致的问题是热门物品具有很强的头部效应,容易跟大量物品产生相似,而尾部物品由于特征向量稀疏,导致很少被推荐。比如下面这个例子:
    第二章 协同过滤(Collaborative Filtering) - 图45
    A, B, C, D是物品, 看右边的物品共现矩阵, 可以发现物品D与A、B、C的相似度比较大, 所以很有可能将D推荐给用过A、B、C的用户。 但是物品D与其他物品相似的原因是因为D是一件热门商品, 系统无法找出A、B、C之间相似性的原因是其特征太稀疏, 缺乏相似性计算的直接数据。 所以这就是协同过滤的天然缺陷:推荐系统头部效应明显, 处理稀疏向量的能力弱。
    为了解决这个问题,同时增加模型的泛化能力,2006年,矩阵分解技术(Matrix Factorization, MF)被提出,该方法在协同过滤共现矩阵的基础上,使用更稠密的隐向量表示用户和物品, 挖掘用户和物品的隐含兴趣和隐含特征,在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。 具体细节后面章节会介绍。
  3. 之前介绍的相似度计算方法(jaccard、余弦和皮尔逊)有什么优劣之处
    cosine相似度还是比较常用的,一般效果也不会太差,但是对于评分数据不规范的时候,也就是说,存在有的用户喜欢打高分,有的用户喜欢打低分情况的时候,有的用户喜欢乱打分的情况,这时候consine相似度算出来的结果可能就不是那么准确了,比如下面这种情况:
    第二章 协同过滤(Collaborative Filtering) - 图46
    这时候,如果用余弦相似度进行计算,会发现用户d和用户f比较相似,而实际上,如果看这个商品喜好的一个趋势的话,其实d和e比较相近,只不过e比较喜欢打低分,d比较喜欢打高分。 所以对于这种用户评分偏置的情况,余弦相似度就不是那么好了,可以考虑使用下面的皮尔逊相关系数。
  4. 协同过滤存在什么缺陷?有什么比较好的思路可以解决(缓解)
    协同过滤的特点就是完全没有利用到物品本身或者是用户自身的属性, 仅仅利用了用户与物品的交互信息就可以实现推荐,比较简单高效, 但这也是它的一个短板所在, 由于无法有效的引入用户年龄, 性别,商品描述,商品分类,当前时间,地点等一系列用户特征、物品特征和上下文特征, 这就造成了有效信息的遗漏,不能充分利用其它特征数据。
    为了解决这个问题, 在推荐模型中引用更多的特征,推荐系统慢慢的从以协同过滤为核心到了以逻辑回归模型为核心, 提出了能够综合不同类型特征的机器学习模型。
    演化图左边的时间线梳理完毕:
    第二章 协同过滤(Collaborative Filtering) - 图47

参考资料