2.1.1. 物品的协同过滤

基于物品的协同过滤(ItemCF):cite:linden2003amazon 的核心思想是:用户的兴趣具有连贯性,喜欢某个物品的用户往往也会对相似的物品感兴趣。ItemCF关注的问题是“和你喜欢的物品相似的还有什么”,通过计算物品之间的相似度来生成推荐。

../../_images/itemcf_illustration.svg

图2.1.1 ItemCF 原理示意图

从上图可以看到,当我们要给左方用户推荐物品时,ItemCF会分析T恤和夹克之间的相似性。由于右方三个用户都同时喜欢这两种服装,系统判断它们具有较高的相似性。既然左方用户喜欢T恤,那么夹克就成为了一个不错的推荐选择。

2.1.1.1. ItemCF核心算法

2.1.1.1.1. 物品相似度计算

在大多数实际场景中,只有用户是否对物品有过交互行为的数据(如点击、购买、收藏等),而没有具体的评分信息。ItemCF 使用余弦相似度量化物品间的相似程度:

(2.1.1)\[w_{ij} = \frac{C[i][j]}{\sqrt{|N(i)| \cdot |N(j)|}}\]

其中 \(|N(i)|\) 表示与物品 \(i\) 有交互的用户总数,\(C[i][j]\) 是两个物品的共现次数(同时对两个物品有过行为的用户数)。分母对共现次数进行标准化,防止热门商品占据绝对优势。

2.1.1.1.2. 候选物品推荐

有了物品相似度矩阵,ItemCF 的线上推荐流程如下:

首先,系统会选取用户最近交互的物品作为兴趣种子(通常是几百个),然后为每个种子物品找到最相似的若干个候选物品(如Top-10),这样可以快速生成大量候选集合。接下来计算用户\(u\)对候选物品\(i\)的兴趣分数:

(2.1.2)\[p(u, i) = \sum_{j \in N(u)} w_{ij} \cdot r_{uj}\]

其中\(N(u)\)是用户\(u\)交互过的物品集合,\(w_{ij}\)是物品\(i\)\(j\)的相似度,\(r_{uj}\)表示用户对物品\(j\)的兴趣强度(可以是简单的1,也可以根据交互时间、类型等设置不同权重)。

最终,系统对所有候选物品按兴趣分数排序,选择Top-N物品作为ItemCF通道的推荐结果。

2.1.1.1.3. 计算效率优化

直接计算所有物品对相似度的复杂度为 \(O(|I|^2 \cdot |U|)\),但大多数物品对之间没有共同的用户交互,相似度必然为 0。基于用户-物品倒排表可以大幅提速:为每个用户维护一个物品交互列表,遍历时将列表中的物品两两配对,累加共现矩阵 \(C[i][j]\),再除以 \(\sqrt{|N(i)||N(j)|}\) 得到余弦相似度,只需计算真正有共同用户的物品对。

优化后的复杂度约为 \(O(R \cdot \bar{m})\)\(R\) 为总交互数,\(\bar{m}\) 为用户平均交互物品数),在稀疏数据场景下由于 \(\bar{m} \ll |I|\),效率远高于暴力计算。

2.1.1.1.4. 处理评分数据的相似度计算

前面的余弦相似度适用于隐式反馈场景(如点击、浏览),当有显式评分数据(如5星评分)时,皮尔逊相关系数能更好地捕获物品间的相似性模式 (Sarwar et al., 2001)

(2.1.3)\[w_{ij} = \frac{\sum_{u \in U_{ij}}(r_{ui} - \bar{r}_i)(r_{uj} - \bar{r}_j)}{\sqrt{\sum_{u \in U_{ij}}(r_{ui} - \bar{r}_i)^2}\sqrt{\sum_{u \in U_{ij}}(r_{uj} - \bar{r}_j)^2}}\]

其中\(U_{ij}\)表示同时评价过物品\(i\)\(j\)的用户集合,\(r_{ui}\)是用户\(u\)对物品\(i\)的评分,\(\bar{r}_i\)是物品\(i\)的平均评分。

皮尔逊相关系数通过中心化处理,消除了不同物品评分分布的差异,关注评分的相对趋势而非绝对值。基于它可以预测用户对未接触物品的评分:

(2.1.4)\[\hat{r}_{u,j} = \bar{r}_{j} + \frac{\sum_{k \in S_j} w_{jk}\,\left( r_{u,k} - \bar{r}_{k} \right)}{\sum_{k \in S_j} w_{jk}}\]

这个公式以物品 \(j\) 的平均评分为基准,根据用户对相似物品的评分偏好进行调整。在大规模推荐系统中,出于计算复杂度和数据稀疏性的考虑,多数系统仍倾向于使用余弦相似度,辅以加权或归一化来处理评分差异。

2.1.1.2. ItemCF评分预测示例

表格 表2.1.1 是和 2.1.3节 相同的用户评分数据。

表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

计算物品之间的相似度,以物品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)\).

(2.1.5)\[\begin{split}\begin{aligned} \text{sim}(item5,item1)&=\frac{\sum_{u \in U}(r_{u,item5} - \bar{r}_{item5})(r_{u,item1} - \bar{r}_{item1})}{\sqrt{\sum_{u \in U}(r_{u,item5} - \bar{r}_{item5})^2}\sqrt{\sum_{u \in U}(r_{u,item1} - \bar{r}_{item1})^2}}\\ &=cos((-0.25, 1.75, 0.75, -2.25),(0.25, 1.25, 0.25, -1.75))\\ &=0.96946 \end{aligned}\end{split}\]

根据皮尔逊相关系数,可以找到与物品5最相似的两个物品是物品1和物品4。基于相似物品,根据上面的计算公式 (2.1.4),可以计算出 用户1 对物品5的最终得分是:

(2.1.6)\[\begin{split}\begin{aligned} \hat{r}_{user1,item5}&=\bar{r}_{item5}+\frac{\sum_{k=1}^{2}\left(w_{item5,itemk}\left(r_{user1,itemk}-\bar{r}_{itemk}\right)\right)}{\sum_{k=1}^{2} w_{item5,itemk}}\\ &=\frac{13}{4}+\frac{0.97*(5-3.2)+0.58*(4-3.4)}{0.97+0.58}\\ &=4.6 \end{aligned}\end{split}\]

2.1.1.3. 代码实践

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

下面我们训练和评估完整的ItemCF模型。

from funrec import run_experiment

run_experiment('item_cf')