.. _usercf: 基于用户的协同过滤 ================== 在网上购物时,我们经常会参考其他买家的选择和评价。如果你发现某个用户和你买了同样的T恤和裤子,而且都给了好评,那么当你看到这个用户还买了一双鞋子时,你可能也会对这双鞋子产生兴趣。基于用户的协同过滤(UserCF)正是基于这样一个朴素而有效的想法:\ **具有相似历史行为的用户,未来偏好也相似**\ 。 UserCF是最早提出的协同过滤方法 :cite:`resnick1994grouplens`\ ,它的工作原理很直观:先找到与目标用户购买偏好相似的“邻居用户”,然后基于这些邻居对商品的选择来预测目标用户的偏好。如 :numref:`usercf_illustration` 所示,如果用户A和用户B都购买了衣服和帽子,那么当用户B还购买了裤子和鞋子时,我们就可以推测用户A可能也会对这些商品感兴趣。 .. _usercf_illustration: .. figure:: ../../img/usercf_illustration.svg UserCF 原理示意图 UserCF的实现过程可以分解为两个核心步骤:首先计算用户之间的相似度,找出与目标用户偏好最接近的邻居用户;然后基于这些邻居用户的历史行为,预测目标用户对未购买商品的兴趣程度。接下来我们详细看看每个步骤是如何实现的。 **第一步:用户相似度计算** 要实现UserCF,首先需要回答一个关键问题:如何判断两个用户是否相似?这就需要用到相似度计算。我们来看几种常用的方法。 假设用户\ :math:`u`\ 和用户\ :math:`v`\ 分别对应物品集合\ :math:`N(u)`\ 和\ :math:`N(v)`\ (即他们各自有过行为的物品)。 **杰卡德相似系数**\ :如果你的系统只记录用户是否对物品有过行为(比如是否点击、购买),而没有具体的评分,那么杰卡德系数是个好选择: .. math:: w_{uv} = \frac{|N(u) \cap N(v)|}{|N(u) \cup N(v)|} 这个公式可以直观理解为:分子是两人共同喜欢的物品数量,分母是两人喜欢的物品总数(去重后)。 **余弦相似度**\ :将每个用户看成一个向量,计算向量间的夹角: .. math:: w_{uv} = \frac{|N(u) \cap N(v)|}{\sqrt{|N(u)|\cdot|N(v)|}} 余弦相似度考虑了用户活跃度的差异。 **皮尔逊相关系数**\ :当你有具体的评分数据时(比如5星评分),皮尔逊系数能更好地捕捉用户的偏好模式: .. math:: w_{uv} = \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}} :label: eq-usercf-pearson 这里\ :math:`r_{ui}`\ 表示用户\ :math:`u`\ 对物品\ :math:`i`\ 的评分,\ :math:`\bar{r}_u`\ 是用户\ :math:`u`\ 的平均评分,\ :math:`I`\ 是两个用户都评价过的物品集合。皮尔逊系数通过中心化处理,有效消除了个人评分习惯的差异。有些人习惯给高分(“好人”),有些人比较严格,通过减去各自的平均分,我们关注的是评分的相对变化趋势,而不是绝对数值。 计算完相似度后,我们通常选择相似度最高的K个用户作为“邻居”,这个K值需要仔细调整——太小可能信息不足,太大可能引入噪音。 **第二步:候选物品推荐** 有了相似用户,下一步就是利用他们的偏好来预测目标用户对未交互过的物品的兴趣。 **简单加权平均**\ :最直接的方法是用相似度作为权重,对邻居的评分进行加权平均: .. math:: \hat{r}_{u,p} = \frac{\sum_{v \in S_u} w_{uv} \, r_{v,p}}{\sum_{v \in S_u} w_{uv}} 这里\ :math:`\hat{r}_{u,p}`\ 是预测的用户\ :math:`u`\ 对物品\ :math:`p`\ 的评分,\ :math:`S_u`\ 是邻居用户集合,\ :math:`w_{uv}`\ 是相似度权重,\ :math:`r_{v,p}`\ 是邻居\ :math:`v`\ 对物品\ :math:`p`\ 的实际评分。 **考虑评分偏置的版本**\ :为了进一步消除个人评分习惯的影响,我们可以加入偏置修正: .. math:: \hat{r}_{u,p} = \bar{r}_{u} + \frac{\sum_{v \in S_u} w_{uv} \, (r_{v,p} - \bar{r}_{v})}{\sum_{v \in S_u} w_{uv}} 这里\ :math:`\bar{r}_u`\ 和\ :math:`\bar{r}_v`\ 分别是用户\ :math:`u`\ 和\ :math:`v`\ 的平均评分。这个公式的思路是:先看邻居们对这个物品的评分相比他们平均水平如何,然后根据目标用户的平均评分水平进行调整。 **优化策略:让算法跑得更快** UserCF看起来很简单,但有个大问题:当用户数量很大时,计算所有用户对之间的相似度会非常耗时,时间复杂度达到\ :math:`O(|U|^2)`\ 。 但仔细观察就会发现,很多用户对之间根本没有共同行为的物品,相似度必然为0,计算它们就是浪费时间。我们可以利用这个特点来优化算法。 **基于物品倒排表的优化**\ : 1. **构建倒排表**\ :为每个物品维护一个用户列表,记录哪些用户对这个物品有过行为。这样就可以通过物品快速找到相关用户。 2. **稀疏矩阵计算**\ :创建一个矩阵\ :math:`C[u][v]`\ 来记录用户\ :math:`u`\ 和\ :math:`v`\ 的共同物品数量。遍历每个物品的用户列表,将列表中的用户两两配对,对应的\ :math:`C[u][v]`\ 值加1。 3. **计算最终相似度**\ :矩阵\ :math:`C`\ 给出了余弦相似度公式的分子,再除以分母\ :math:`\sqrt{|N(u)||N(v)|}`\ 就得到了用户相似度。 4. **生成推荐**\ :使用以下公式计算用户\ :math:`u`\ 对物品\ :math:`i`\ 的兴趣分数: .. math:: p(u, i) = \sum_{v \in S_u \cap N(i)} w_{uv} r_{vi} 其中\ :math:`S_u`\ 是与用户\ :math:`u`\ 最相似的\ :math:`K`\ 个用户集合,\ :math:`N(i)`\ 是对物品\ :math:`i`\ 有过行为的用户集合。实际推荐时,针对目标用户未交互过的物品计算上述兴趣度量值,并按分值降序排列,选择Top-N物品作为推荐结果。 这个优化的效果如何呢?新算法的时间复杂度约为\ :math:`O(R \cdot \bar{n})`\ ,其中\ :math:`R`\ 是总的用户-物品交互记录数,\ :math:`\bar{n}`\ 是每个物品的平均用户数。在稀疏数据场景下(即\ :math:`R \ll |U|^2`\ 且\ :math:`\bar{n} \ll |U|`\ )),这比原来的\ :math:`O(|U|^2)`\ 快得多。 应用实践 -------- 1.数据集。表格 :numref:`table-usercf-data` 里是5个用户对5个物品的评分数据,可以理解为用户对物品的偏好程度。 .. _table-usercf-data: .. table:: 用户评分数据 ===== ===== ===== ===== ===== ===== \ 物品1 物品2 物品3 物品4 物品5 ===== ===== ===== ===== ===== ===== 用户1 5 3 4 4 ? 用户2 3 1 2 3 3 用户3 4 3 4 3 5 用户4 3 3 1 5 4 用户5 1 5 5 2 1 ===== ===== ===== ===== ===== ===== 2.手动分析。基于皮尔逊相关系数,计算用户1 与其他用户(以用户2为例)的相似度。 - 计算用户1与用户2的皮尔逊相关系数: :math:`\bar{r}_{user1}=4,\ \bar{r}_{user2}=2.25`\ 。向量减去均值: :math:`\text{user1}:(1,-1, 0,0) \quad \text{user2}: (0.75,-1.25,-0.25,0.75)` - 计算这俩新向量的余弦相似度,得到皮尔逊相似度 :math:`0.852`\ 。 根据相似用户,计算用户1对物品5的最终得分。用户2对物品5的评分是3, 用户3对物品5的打分是5, 那么根据上面的计算公式 :eq:`eq-usercf-pearson`\ ,可以计算出 用户1 对物品5的最终得分是: .. math:: P_{user1, item5}=\bar{r}_{user1}+\frac{\sum_{k=1}^{2}\left(w_{user1,user k}\left(r_{userk, item5}-\bar{r}_{userk}\right)\right)}{\sum_{k=1}^{2} w_{user1, userk}}=4+\frac{0.85*(3-2.4)+0.7*(5-3.8)}{0.85+0.7}=4.87 代码实践 -------- 1. 数据准备 .. raw:: latex \diilbookstyleinputcell .. code:: python import numpy as np user_data = { 'user1': {'item1': 5, 'item2': 3, 'item3': 4, 'item4': 4}, 'user2': {'item1': 3, 'item2': 1, 'item3': 2, 'item4': 3, 'item5': 3}, 'user3': {'item1': 4, 'item2': 3, 'item3': 4, 'item4': 3, 'item5': 5}, 'user4': {'item1': 3, 'item2': 3, 'item3': 1, 'item4': 5, 'item5': 4}, 'user5': {'item1': 1, 'item2': 5, 'item3': 5, 'item4': 2, 'item5': 1}, } 这里使用字典来建立用户-物品的交互表: - 字典users的键表示不同用户的名字,值为一个评分字典,评分字典的键值对表示某物品被当前用户的评分。 - 由于现实场景中,用户对物品的评分比较稀疏。如果直接使用矩阵进行存储,会存在大量空缺值,故此处使用了字典。 2.基于皮尔逊相关系数,计算用户相似性矩阵 .. raw:: latex \diilbookstyleinputcell .. code:: python # 初始化相似性矩阵 import pandas as pd similarity_matrix = pd.DataFrame( np.identity(len(user_data)), index=user_data.keys(), columns=user_data.keys(), ) # 遍历每条用户-物品评分数据 for u1, items1 in user_data.items(): for u2, items2 in user_data.items(): if u1 == u2: continue vec1, vec2 = [], [] for item, rating1 in items1.items(): rating2 = items2.get(item, -1) if rating2 == -1: continue vec1.append(rating1) vec2.append(rating2) # 计算不同用户之间的皮尔逊相关系数 similarity_matrix[u1][u2] = np.corrcoef(vec1, vec2)[0][1] print(similarity_matrix) 3.计算用户1最相似的n个用户 .. raw:: latex \diilbookstyleinputcell .. code:: python target_user = 'user1' num = 2 # 由于最相似的用户为自己,去除本身 sim_users = similarity_matrix[target_user].sort_values(ascending=False)[1:num+1].index.tolist() print(f'与用户{target_user}最相似的{num}个用户为:{sim_users}') 4. 预测用户1对物品5的评分 .. raw:: latex \diilbookstyleinputcell .. code:: python weighted_scores = 0. corr_values_sum = 0. target_item = 'item5' # 基于皮尔逊相关系数预测用户评分 for user in sim_users: corr_value = similarity_matrix[target_user][user] user_mean_rating = np.mean(list(user_data[user].values())) weighted_scores += corr_value * (user_data[user][target_item] - user_mean_rating) corr_values_sum += corr_value target_user_mean_rating = np.mean(list(user_data[target_user].values())) target_item_pred = target_user_mean_rating + weighted_scores / corr_values_sum print(f'用户{target_user}对物品{target_item}的预测评分为:{target_item_pred:.4f}') 5. 训练模型 .. raw:: latex \diilbookstyleinputcell .. code:: python import os import sys import funrec from funrec.utils import build_metrics_table # 加载配置 config = funrec.load_config('user_cf') # 加载数据 train_data, test_data = funrec.load_data(config.data) # 准备特征 feature_columns, processed_data = funrec.prepare_features(config.features, train_data, test_data) # 训练模型 models = funrec.train_model(config.training, feature_columns, processed_data) # 评估模型 metrics = funrec.evaluate_model(models, processed_data, config.evaluation, feature_columns) print(build_metrics_table(metrics)) .. raw:: latex \diilbookstyleoutputcell .. parsed-literal:: :class: output +---------------+--------------+----------------+---------------+ | hit_rate@10 | hit_rate@5 | precision@10 | precision@5 | +===============+==============+================+===============+ | 0.6912 | 0.5927 | 0.1643 | 0.2063 | +---------------+--------------+----------------+---------------+