本内容参考项亮的《推荐系统实战》与DataWhale的fun-rec

1 协同过滤简介

1.1 什么是协同过滤?

协同过滤(Collaborative Filtering)是经典的推荐算法。

其思想是根据用户历史的喜好及其他兴趣类似的用户的选择,来推荐商品

协调过滤通常仅基于用户的行为(例如评分、购买及点击等等), 不会依赖任何附加的信息(例如商品的自身特征)或用户的自身信息(例如年龄、性别等等)。

基于邻域的协同过滤是较常用的算法,主要包括以下两种:

  • 基于用户的协同过滤算法(UserCF): 推荐与当前用户兴趣相似的用户喜欢的商品
  • 基于商品的协同过滤算法(ItemCF): 推荐与用户历史喜欢的商品相似的商品

协同过滤最重要的步骤是计算用户或商品间的相似度。常见相似度包括:

  • Jaccard 相似系数
  • 余弦相似度
  • Pearson相似度

1.2 相似性度量方法

1.2.1 Jaccard相似系数

Jaccard相似系数指的是:用户$u$与$v$喜欢的商品的交集除并集。其中,$N(u),N(v)$表示用户$u$和用户$v$喜欢的商品的集合。

$$ sim_{uv}=\frac{|N(u) \cap N(v)|}{|N(u)| \cup|N(v)|} $$

Jaccard相似系数不会反应用户的评分,仅用来做二元评估(例如用户是否会购买该产品)。

1.2.2 余弦相似度

余弦相似度计算的是两个向量的夹角,夹角越小则相似度越高。

假设$u,v$分别是用户$u,v$对某商品的评分(也可以是是否购买,购买为1,未购买为0),那么两个人的余弦相似度的计算公式为:

$$ sim_{uv} = cos(u,v) =\frac{u\cdot v}{|u|\cdot |v|} $$

代码示例:

from sklearn.metrics.pairwise import cosine_similarity

a = [[1, 0, 1, 0]]
b = [[1, 0, 1, 1]]
c = [[0, 1, 0, 1]]

print('cosine_similarity of (a, b):\n', cosine_similarity(a, b))

print('cosine_similarity of (a, c):\n', cosine_similarity(a, c))

print('cosine_similarity of (b, c):', cosine_similarity(b, c))
cosine_similarity of (a, b):
 [[0.81649658]]
cosine_similarity of (a, c):
 [[0.]]
cosine_similarity of (b, c): [[0.40824829]]

1.2.3 Pearson相关系数

Pearson相关系数与余弦相似度类似,公式如下:

如下是皮尔逊相关系数计算公式,其中$r_{ui},r_{vi}$表示用户$u$与$v$对商品$i$的评分,$\bar r_u, \bar r_v$分别表示用户$u$和$v$评分的均值或商品数。

$$ sim(u,v)=\frac{\sum_{i\in I}(r_{ui}-\bar r_u)(r_{vi}-\bar r_v)}{\sqrt{\sum_{i\in I }(r_{ui}-\bar r_u)^2}\sqrt{\sum_{i\in I }(r_{vi}-\bar r_v)^2}} $$

所以相比余弦相似度,皮尔逊相关系数通过使用用户的平均分对各独立评分进行修正,减小了用户评分偏置的影响。具体实现, 我们也是可以调包, 这个计算方式很多, 下面是其中的一种:

from scipy.stats import pearsonr

a = [1, 0, 1, 0]
b = [1, 0, 1, 1]
c = [0, 1, 0, 1]

print('pearsonr of (a, b):\n', pearsonr(a, b))

print('pearsonr of (a, c):\n', pearsonr(a, c))

print('pearsonr of (b, c):', pearsonr(b, c))
pearsonr of (a, b):
 (0.5773502691896258, 0.4226497308103741)
pearsonr of (a, c):
 (-1.0, 0.0)
pearsonr of (b, c): (-0.5773502691896258, 0.4226497308103741)

2 基于用户的协同过滤(UserCF)

2.1 UserCF介绍

UserCF的思想是,找到与目标用户的兴趣相似的其他用户, 把这些用户喜欢,而目标用户没有见过的商品推荐给他。

UserCF的过程如下:

  1. 基于前面提出的相似度方法,找出兴趣相似的用户
  2. 对相似用户喜欢的商品计算喜好程度,并把喜好程度高的商品推荐给客户

那么有一个问题,如何衡量喜欢程度呢?以一个例子距离。

以下面图片为例,我们想计算出Alice对 Item 5的打分。

那么UserCF的操作步骤如下:

  1. 分别计算Alice和User 1,2,3,4的相似度, 找出与Alice最相似的n个用户
  2. 根据这n个User对Item 5的评分情况以及他们与Alicer的相似程度,猜测Alice对Item 5的评分。
  3. 如果Item 5的评分较高的话, 则为Alice推荐Item 5

第1步可以用1.2中介绍的方法计算相似度。

第2步常用的计算方法是加权平均,公式为:

$$ R_{\mathrm u, \mathrm p}=\frac{\sum_{\mathrm{s} \in S}\left(w_{\mathrm{u}, \mathrm{s}} \cdot R_{\mathrm{s}, \mathrm{p}}\right)}{\sum_{\mathrm{s} \in S} w_{\mathrm{u}, \mathrm{s}}} $$

其中,权重$w_{\mathrm u,\mathrm s}$是用户$u$和用户$s$的相似度, $R_{\mathrm s,\mathrm p}$是用户$s$对商品$p$的评分。

除了加权平均法,还有另一种方法,公式如下:

$$ P_{i, j}=\bar{R}_{i}+\frac{\sum_{k=1}^{n}\left(w_{j, k}\left(R_{k, j}-\bar{R}_{k}\right)\right)}{\sum_{k=1}^{n} S_{j, k}} $$

  • $P_{ij}$:第$i$个用户对第$j$个商品的评分预测
  • $S_{j, k}$:用户$j$对第$k$个商品的评分
  • $\bar{R}_{k}$:第$k$用户对全部商品评分的均值

这个方法依旧使用相似度作为权值,但乘的值是$R_{k, j}-\bar{R}_{k}$,也就是减去了这个用户的评分均值,这样一来,避免有些用户打分普遍较低或者普遍较高。

这种计算方式考虑的更加全面,因此也更为常用。

2.2 编程实现

  1. 首先,建立数据表
    这里采用了字典的方式, 之所以没有用pandas, 是因为上面举得这个例子其实是个个例, 在真实情况中, 我们知道, 用户对商品的打分情况并不会这么完整, 会存在大量的空值, 所以矩阵会很稀疏, 这时候用DataFrame, 会有大量的NaN。故这里用字典的形式存储。 用两个字典, 第一个字典是商品-用户的评分映射, 键是商品1-5, 用A-E来表示, 每一个值又是一个字典, 表示的是每个用户对该商品的打分。 第二个字典是用户-商品的评分映射, 键是上面的五个用户, 用1-5表示, 值是该用户对每个商品的打分。
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. 计算用户相似性矩阵

利用三重循环计算

  • 第一重循环是要计算similarity的user
  • 第二重循环是其他user
  • 第三重循环是每一个item
  • 如果两个user都对这个item评分过,那么就把评分加到list中
  • 最后根据这些评分计算相关性
"""计算用户相似性矩阵"""
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就是我们的用户相似性矩阵, 长下面这样:

有了相似性矩阵, 我们就可以得到与Alice最相关的前n个用户。

  1. 计算前n个相似的用户
"""计算前n个相似的用户"""
n = 2
similarity_users = similarity_matrix[1].sort_values(ascending=False)[:n].index.tolist()    # [2, 3]   也就是用户1和用户2
  1. 计算最终得分
    这里就是上面的那个公式了。
"""计算最终得分"""
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.3 UserCF优缺点

UserCF算法存在两个重大问题:

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

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

3 基于商品的协同过滤(ItemCF)

ItemCF介绍

ItemCF的基本思想,是计算商品间的相似性。如果喜欢商品a的用户同时也喜欢商品c,而用户A喜欢商品a,那么就把商品c推荐给用户A。

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

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

基于ItemCF和基于UserCF很像, 继续以上面的Alice问题的为例。

如果想知道Alice对商品5打多少分, 基于商品的协同过滤算法会这么做:

  1. 首先计算Item 5和 Item 1,2,3,4 之间的相似性

它们也是向量的形式, 每一列的值就是它们的向量表示, 因为ItemCF认为商品a和商品c具有很大的相似度是因为喜欢商品a的用户大都喜欢商品c, 所以就可以基于每个用户对该商品的打分或者说喜欢程度来向量化商品

  1. 找出与商品5最相近的n个商品
  2. 根据Alice对最相近的n个商品的打分去计算对商品5的打分情况

下面我们就可以具体计算一下, 首先是步骤1:

由于计算比较麻烦, 这里直接用python计算了:

根据皮尔逊相关系数, 可以找到与item 5最相似的2个商品是item 1和item 4(n=2), 下面基于上面的公式计算最终得分:

$$ P_{Alice, 物品5}=\bar{R}_{物品5}+\frac{\sum_{k=1}^{2}\left(S_{物品5,物品 k}\left(R_{Alice, 物品k}-\bar{R}_{物品k}\right)\right)}{\sum_{k=1}^{2} S_{物品k, 物品5}}=\frac{13}{4}+\frac{0.97*(5-3.2)+0.58*(4-3.4)}{0.97+0.58}=4.6 $$

这时候依然可以向Alice推荐item 5。下面也是简单编程实现一下, 和上面的差不多:

"""计算商品的相似矩阵"""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]

商品的相似度矩阵, 大概是下面这个样子:

然后也是得到与商品5相似的前n个商品, 计算出最终得分来。

"""得到与商品5相似的前n个商品"""
n = 2
similarity_items = similarity_matrix['E'].sort_values(ascending=False)[:n].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

结果如下:

4 UserCF与ItemCF算法评估

由于UserCF和ItemCF结果评估部分是共性知识点, 所以在这里统一标识。 这里介绍评测指标:

  1. 召回率

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

$$ \operatorname{Recall}=\frac{\sum_{u}|R(u) \cap T(u)|}{\sum_{u}|T(u)|} $$

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

  1. 准确率
    准确率定义为:

$$ \operatorname{Precision}=\frac{\sum_{u} \mid R(u) \cap T(u)|}{\sum_{u}|R(u)|} $$

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

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

$$ \text { Coverage }=\frac{\left|\bigcup_{u \in U} R(u)\right|}{|I|} $$

  1. 该覆盖率表示最终的推荐列表中包含多大比例的商品。如果所有商品都被给推荐给至少一个用户, 那么覆盖率是100%。
  2. 新颖度
    用推荐列表中商品的平均流行度度量推荐结果的新颖度。 如果推荐出的商品都很热门, 说明推荐的新颖度较低。 由于商品的流行度分布呈长尾分布, 所以为了流行度的平均值更加稳定, 在计算平均流行度时对每个商品的流行度取对数。

8. 协同过滤算法的权重改进

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

9. 协同过滤算法的问题分析

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

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

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

参考资料

  1. 《推荐系统实战》
  2. https://github.com/datawhalechina/fun-rec/blob/master/docs
最后修改:2021 年 11 月 22 日 10 : 20 PM
如果觉得我的文章对你有用,请随意赞赏