推荐系统-1 基于协同过滤的召回

134 阅读2分钟

协同过滤算法利用“相似用户喜欢相似物品 or 相似物品被同一用户喜欢”这一协同信号,对用户未接触过的物品进行兴趣预测与推荐。

相似性度量方法

  • u/vUseru/v - User
  • i/jItemi/j - Item

杰卡德相似度

Jac(u,v)=IuIvIuIv\text{Jac}(u,v) = \frac{|I_{\color{blue}u} \cap I_{\color{green}v}|}{|I_{\color{blue}u} \cup I_{\color{green}v}|}

IuI_u 表示用户 u 交互过的物品集合,UiU_i 表示跟物品 i 交互过的用户集合。

适用于隐式反馈数据(0-1)

余弦相似度

Cos(u,v)=iru,irv,iiru,i2irv,i2\text{Cos}(u, v) = \frac{\sum\limits_{\color{red}i} r_{\color{blue}u,\color{red}i} \, r_{\color{green}v,\color{red}i}} {\sqrt{\sum\limits_{\color{red}i} r^2_{\color{blue}u,\color{red}i}} \, \sqrt{\sum\limits_{\color{red}i} r^2_{\color{green}v,\color{red}i}}}

ru,ir_{u,i} 表示用户 u 对物品 i 的分数。

皮尔逊相似度

Pear(u,v)=i(ru,iru)(rv,irv)i(ru,iru)2i(rv,irv)2\text{Pear}(u,v) = \frac{ \sum_{\color{red}i} \left(r_{{\color{blue}u},{\color{red}i}} - \overline{r_{\color{blue}u}}\right) \left(r_{{\color{green}v},{\color{red}i}} - \overline{r_{\color{green}v}}\right) }{ \sqrt{ \sum_{\color{red}i} \left(r_{{\color{blue}u},{\color{red}i}} - \overline{r_{\color{blue}u}}\right)^2 } \sqrt{ \sum_{\color{red}i} \left(r_{{\color{green}v},{\color{red}i}} - \overline{r_{\color{green}v}}\right)^2 } }

ru\overline{r_u} 为用户 u 的所有评分均值。

皮尔逊系数本质是“中心化后”的余弦,换句话说,把每个维度减去各自用户的均值再算余弦,就能得到皮尔逊,这就是皮尔逊能消除“有人爱打高分,有人爱打低分”的打分尺度差异(偏置)的原因。不适用于计算布尔值向量(0-1)之间相关度。

算法评估

召回率

Recall@K=RuGuGu\text{Recall}@K = \frac{|R_u \cap G_u|}{|G_u|}
  • Ru: R_u: 系统对用户 u 返回的前K个推荐的物品集合
  • Gu: G_u: 用户 u 实际感兴趣的物品集合(ground truth)

在给定长度为K的候选列表中,系统成功找回了用户真实感兴趣物品的比例

精确率

Precision@K=RuGuK\text{Precision}@K = \frac{|R_u \cap G_u|}{K}

返回的前K个候选中,真正感兴趣物品所占比例

UserCF

基于用户的协同过滤(UserCF):找出和目标用户兴趣模式最相似的一批用户(邻居),用他们对物品的偏好来推测目标用户可能喜欢什么。

ABCD
U₁534
U₂43
U₃45

假设以用户U1U_1为目标用户,推荐他还未评分的物品(相似度用皮尔逊相关系数)。

  • rˉU1=5+3+43=4.0\bar{r}_{U_1} = \frac{5 + 3 + 4}{3} = 4.0
  • rˉU2=4+32=3.5\bar{r}_{U_2} = \frac{4 + 3}{2} = 3.5
  • rˉU3=4+52=4.5\bar{r}_{U_3} = \frac{4 + 5}{2} = 4.5
  • 对于U1U2U_1、U_2而言,共评物品为A。
sim(U1,U2)=(54)(43.5)(54)2(43.5)2=10.510.25=0.50.5=1.0\text{sim}(U_1, U_2) = \frac{(5 - 4)(4 - 3.5)}{ \sqrt{(5 - 4)^2} \cdot \sqrt{(4 - 3.5)^2} } = \frac{1 \cdot 0.5}{\sqrt{1} \cdot \sqrt{0.25}} = \frac{0.5}{0.5} = 1.0
  • 对于U1U3U_1、U_3而言,共评物品为B,D。
sim(U1,U3)=(34)(44.5)+(44)(54.5)(34)2+(44)2(44.5)2+(54.5)2=(1)(0.5)+(0)(0.5)1+00.25+0.25=0.510.50.50.70710.7071\begin{aligned} \text{sim}(U_1, U_3) &= \frac{(3 - 4)(4 - 4.5) + (4 - 4)(5 - 4.5)}{ \sqrt{(3 - 4)^2 + (4 - 4)^2} \cdot \sqrt{(4 - 4.5)^2 + (5 - 4.5)^2}} \\ &= \frac{(-1)(-0.5) + (0)(0.5)}{ \sqrt{1 + 0} \cdot \sqrt{0.25 + 0.25}} \\ &= \frac{0.5}{1 \cdot \sqrt{0.5}} \approx \frac{0.5}{0.7071} \approx 0.7071 \end{aligned}

对于用户U1U_1,我们需要得到他对物品C的评分。

  1. 若不考虑用户评分的偏置。
RU1,C=1.0RU2,C1.0=131=3.0\text{R}_{U_1,C} = \frac{1.0 \cdot \text{R}_{U_2,C}}{1.0} = \frac{1 \cdot 3}{1} = 3.0
  1. 考虑用户评分偏置。
RU1,C=RˉU1+1.0(33.5)1.0=4.0+(0.5)=3.5R_{U_1, C} = \bar{R}_{U_1} + \frac{1.0 \cdot (3 - 3.5)}{1.0} = 4.0 + (-0.5) = 3.5

UserCF存在的两个问题:

  1. 数据稀疏性问题:在大型电商平台中,物品种类极多,而用户实际购买的物品只占很小一部分,往往不到 1%。这导致用户之间的交集很少,相似用户难以匹配
  2. 算法扩展性差:UserCF 需要维护一个完整的用户相似度矩阵,用于查找 Top-N 相似用户。这使得存储成本高,计算代价大。 因此,UserCF 不适用于用户规模大、反馈稀疏的系统,这也是为什么很多电商平台在早期推荐系统中更倾向使用 Item-based CF(ItemCF) 来代替 UserCF。

ItemCF

基于物品的协同过滤(ItemCF):两个物品若被同一批用户共同交互,它们就相似;因此若用户喜欢物品A,则它的相似物品B也有可能被用户喜欢。

  • 离线阶段:根据用户-物品行为日志,计算物品-物品相似度矩阵

  • 在线阶段:对目标用户,找出其已交互物品的相似物品并加权汇总,生成 Top-N 推荐列表

ItemCF和UserCF的算法过程基本一致,唯一的差别是,需要计算的是物品之间的相似度,然后根据目标物品和其他物品的相似度找到top-k个物品,再计算用户对物品的评分。

UserCF:由于是基于用户相似度进行推荐, 所以具备更强的社交特性, 这样的特点非常适于用户少, 物品多, 时效性较强,追求热点的场合。比如新闻推荐
ItemCF:- 这个更适用于兴趣变化较为稳定的应用, 更接近于个性化的推荐, 适合物品少,用户多,用户兴趣固定持久, 物品更新速度不是太快的场合。

Swing - 改进的ItemCF

ItemCF存在什么问题?

基于相似度的方法常用于Item-based CF,但是Item-based CF的核心是“用户喜欢A,那么就推荐与A相似的物品B”,这个过程逻辑上等价于走一条路径

Itemi被哪些用户点击/评分Useru他还点击/评分了什么ItemjItem_i \xrightarrow{\text{被哪些用户点击/评分}} User_u \xrightarrow{\text{他还点击/评分了什么}} Item_j

但是这个算法把所有 User-Item 行为都“平等对待”

  • 并没有区分哪些点击是认真看了,哪种是无意点击。因为实际上每个用户的点击行为价值是不同的。比如用户A是泛娱乐型,什么都点,信号很“水”。用户B只点科技类,信号更加有价值。

因此,容易引入很多噪声,影响推荐精度,存在局限性。

并且对互补性产品的建模不足,可能会导致用户购买过手机之后还继续推荐手机,但用户短时间内不会再继续购买手机,因此产生无效曝光。

Swing算法

Swing算法本质是一种改进的item-item协同过滤算法,通过考虑“用户对”之间的兴趣差异,对物品的相似度进行更精细的加权,从而提升推荐精度和多样性。
Swing的核心思想在于用户对越不相似,它们共同点击两个物品的行为越有价值,应该被重点考虑
Swing相似度公式:

sim(i,j)=u,vUiUj1α+IuIv\text{sim}(i, j) = \sum_{u,v \in U_i \cap U_j} \frac{1}{\alpha + |I_u \cap I_v|}

其中:UiU_i为点击过物品i的用户集合,IuI_u为用户u点击过的物品集合,α\alpha为平滑系数,IuIv|I_u \cap I_v|为用户uuvv的物品交集数。

α\alpha(平滑系数)的作用

  1. 避免分母为0。若IuIv=I_u \cap I_v = \emptyset,分母为0,会导致除零错误
  2. 约束冷门用户对。 小 α → 冷门用户对的贡献上限越大,模型更敏感、推荐更“激进”。
    大 α → 相似度差异被拉平,收敛得更慢、更加保守
  3. 二次惩罚热门用户对
    对热门用户(交集大),原本分母就大;再 +α\alpha 后分母更大,所以这个 α\alpha 可以进一步削弱热门用户对带来的影响。
from itertools import combinations

user_score_dict = {
    'A': {'a': 3.0, 'b':4.0, 'c':0.0, 'd':3.5, 'e':0.0},
    'B': {'a': 4.0, 'b':0.0, 'c':4.5, 'd':0.0, 'e':3.5},
    'C': {'a': 0.0, 'b':3.5, 'c':0.0, 'd':0.0, 'e':3.0},
    'D': {'a': 0.0, 'b':4.0, 'c':0.0, 'd':3.5, 'e':3.0},
}

item_user_count = dict()  # 每个物品有多少用户产生过行为
user_item_count = dict()  # 每个用户交互过哪些物品

for user, score_dict in user_score_dict.items():
    for item, score in score_dict.items():
        user_item_count.setdefault(user, set())
        item_user_count.setdefault(item, set())
        if score > 0:
            user_item_count[user].add(item)
            item_user_count[item].add(user)

item_pairs = list(combinations(item_user_count.keys(), 2))
alpha = 0.5
item_sim_dict = dict()
for (i, j) in item_pairs:
    user_pair = list(combinations(item_user_count[i] & item_user_count[j], 2))
    result = 0
    for (u, v) in user_pair:
        result += 1 / (alpha + len(item_user_count[i] & item_user_count[j]))
    item_sim_dict.setdefault(i, dict())
    item_sim_dict[i][j] = result
    item_sim_dict.setdefault(j, dict())
    item_sim_dict[j][i] = result

# 计算用户对物品的评分
def UserItemScore(user, item):
    score = 0
    for i in user_item_count[user]:
        if i == item:
            continue
        score += item_sim_dict[i][item] * user_score_dict[user][i]
    return score

# 为用户推荐物品
def recommend(user):
    user_item_score = dict()
    for item in item_user_count.keys():
        user_item_score[item] = UserItemScore(user, item)
    # user_item_score = sorted(user_item_score.items(), key=lambda x: x[1], reverse=True)
    return user_item_score

print(recommend('C'))

基于swing算法的电影推荐系统实现:github.com/yqqCheergo/…

矩阵分解