2.1.2. 基于物品的协同过滤¶
当你在购物网站上买了一件T恤后,系统往往会推荐夹克等其他服装。这背后的逻辑很直观:既然你喜欢这件T恤,那么与它相似的其他服装可能也符合你的品味。这就是基于物品的协同过滤(ItemCF)的核心思想 (Linden et al., 2003)。
与前面介绍的UserCF不同,ItemCF换了一个角度来思考推荐问题。它不再寻找“和你相似的用户还喜欢什么”,而是关注“和你喜欢的物品相似的还有什么”。这种思路建立在一个简单的假设上:用户的兴趣具有一定的连贯性,喜欢某个物品的用户往往也会对相似的物品感兴趣。
图2.1.2 ItemCF 原理示意图¶
从上图可以看到,当我们要给左方用户推荐物品时,ItemCF会分析T恤和夹克之间的相似性。由于右方两个用户都同时喜欢这两种服装,系统判断它们具有较高的相似性。既然左方用户喜欢T恤,那么夹克就成为了一个不错的推荐选择。
ItemCF的实现流程主要包含以下两个步骤:
第一步:物品相似度计算
要实现ItemCF,首先需要量化物品之间的相似程度。在大多数实际应用场景中,我们通常只有用户是否对物品有过交互行为的数据(如点击、购买、收藏等),而没有具体的评分信息。
在理论上,我们可以将每个物品表示为一个用户向量,然后计算向量间的相似度。但当商品数量巨大时,计算所有物品对之间的相似度会变成一个巨大的工程,时间复杂度达到\(O(|I|^2)\)。
实际上,很多物品对之间没有共同的用户交互,它们的相似度必然为0,计算它们就是浪费时间。因此我们从用户出发找物品组合,采用更高效的实现方式:
构建用户-物品倒排表:为每个用户维护一个交互过的物品列表。
计算物品共现矩阵:创建一个矩阵\(C[i][j]\)来记录物品\(i\)和\(j\)的共同用户数量。遍历所有用户的物品列表,将列表中的物品两两配对,对应的\(C[i][j]\)值加1,这就构成了共现矩阵。
计算最终相似度:使用余弦相似度公式计算物品相似度:
这里\(|N(i)|\)表示与物品\(i\)有交互的用户总数,\(C[i][j]\)是两个物品的共现次数。这个公式很直观:分子是两个物品的共同用户数,分母是对共同用户数的标准化,防止热门商品占据绝对优势。
这种实现方式的时间复杂度约为\(O(R \cdot \bar{m})\),其中\(R\)是用户-物品交互记录总数,\(\bar{m}\)是用户平均交互的物品数量。在数据稀疏的实际场景中,这比直接计算所有物品对的\(O(|I|^2)\)要高效得多。
第二步:候选物品推荐
有了物品相似度矩阵,我们就能预测用户对未接触物品的喜好程度了。计算用户\(u\)对物品\(i\)的兴趣分数:
其中\(S_i\)是与物品\(i\)最相似的\(K\)个物品集合,\(N(u)\)是对物品\(i\)有过行为的用户集合。实际推荐时,针对目标用户未交互过的物品计算上述兴趣度量值,并按分值降序排列,选择Top-N物品作为推荐结果。
处理评分数据:皮尔逊相关系数
在某些应用场景中,我们不仅知道用户是否与物品有交互,还有具体的评分信息(如5星评分、点赞数等)。这时候可以使用更细致的相似度计算方法。
皮尔逊相关系数:当有评分数据时,可以使用皮尔逊相关系数来衡量物品间的相似性 (Sarwar et al., 2001):
这个公式的含义是:我们取出同时评价过物品\(i\)和物品\(j\)的所有用户,比较他们对这两个物品的评分模式。其中\(r_{ui}\)表示用户\(u\)对物品\(i\)的评分,\(\bar{r}_i\)表示物品\(i\)收到的平均评分。如果用户们对两个物品的评分趋势一致(都高或都低),相似度就会较高。
基于评分的预测:有了基于评分的相似度,我们可以更精确地预测用户对未接触物品的评分:
这个预测公式的逻辑是:先以物品\(j\)的平均评分作为基准,然后根据用户\(u\)对相似物品的评分偏好进行调整。\(S_j\)表示与物品\(j\)最相似的物品集合,\(w_{jk}\)是物品\(j\)和\(k\)之间的相似度权重。
皮尔逊相关系数通过中心化处理,有效消除了不同物品评分分布的差异,能够更好地捕获物品间的相似性模式。
2.1.2.1. 应用实践¶
1.数据集 表格 表2.1.2 是和 2.1.1节 相同的用户评分数据。
用户1 |
用户2 |
用户3 |
用户4 |
用户5 |
|
---|---|---|---|---|---|
物品1 |
5 |
3 |
4 |
3 |
1 |
物品2 |
3 |
1 |
3 |
3 |
5 |
物品3 |
4 |
2 |
4 |
1 |
5 |
物品4 |
4 |
3 |
3 |
5 |
2 |
物品5 |
? |
3 |
5 |
4 |
1 |
2.手动分析 计算物品之间的相似度,以物品5和物品1之间的皮尔逊相关系数为例。\(\hat{r}_{item5}=3.25,\ \hat{r}_{item1}=2.75\), 向量减去均值: \(\text{item5}:(-0.25, 1.75, 0.75, -2.25) \quad \text{item1}: (0.25, 1.25, 0.25, -1.75)\).
根据皮尔逊相关系数,可以找到与物品5最相似的两个物品是物品1和物品4。基于相似物品,根据上面的计算公式 (2.1.11),可以计算出 用户1 对物品5的最终得分是:
2.1.2.2. 代码实践¶
数据准备
import numpy as np
item_data = {
'item1': {'user1': 5, 'user2': 3, 'user3': 4, 'user4': 3, 'user5': 1},
'item2': {'user1': 3, 'user2': 1, 'user3': 3, 'user4': 3, 'user5': 5},
'item3': {'user1': 4, 'user2': 2, 'user3': 4, 'user4': 1, 'user5': 5},
'item4': {'user1': 4, 'user2': 3, 'user3': 3, 'user4': 5, 'user5': 2},
'item5': {'user2': 3, 'user3': 5, 'user4': 4, 'user5': 1},
}
计算物品间的相似度矩阵
import pandas as pd
similarity_matrix = pd.DataFrame(
np.identity(len(item_data)),
index=item_data.keys(),
columns=item_data.keys(),
)
# 遍历每条物品-用户评分数据
for i1, users1 in item_data.items():
for i2, users2 in item_data.items():
if i1 == i2:
continue
vec1, vec2 = [], []
for user, rating1 in users1.items():
rating2 = users2.get(user, -1)
if rating2 == -1:
continue
vec1.append(rating1)
vec2.append(rating2)
similarity_matrix[i1][i2] = np.corrcoef(vec1, vec2)[0][1]
print(similarity_matrix)
评分预测 从user1交互过的物品中,找到与item5最相似的2个物品
target_user = 'user1'
target_item = 'item5'
num = 2
sim_items = []
sim_items_list = similarity_matrix[target_item].sort_values(ascending=False).index.tolist()
for item in sim_items_list:
# 如果target_user对物品item评分过
if target_user in item_data[item]:
sim_items.append(item)
if len(sim_items) == num:
break
print(f'与物品{target_item}最相似的{num}个物品为:{sim_items}')
预测用户1对物品5的评分
target_user_mean_rating = np.mean(list(item_data[target_item].values()))
weighted_scores = 0.
corr_values_sum = 0.
for item in sim_items:
corr_value = similarity_matrix[target_item][item]
user_mean_rating = np.mean(list(item_data[item].values()))
weighted_scores += corr_value * (item_data[item][target_user] - user_mean_rating)
corr_values_sum += corr_value
target_item_pred = target_user_mean_rating + weighted_scores / corr_values_sum
print(f'用户{target_user}对物品{target_item}的预测评分为:{target_item_pred}')
训练模型
import os
import sys
import funrec
from funrec.utils import build_metrics_table
# 加载配置
config = funrec.load_config('item_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))
+---------------+--------------+----------------+---------------+
| hit_rate@10 | hit_rate@5 | precision@10 | precision@5 |
+===============+==============+================+===============+
| 0.6594 | 0.5459 | 0.1444 | 0.1826 |
+---------------+--------------+----------------+---------------+