协同过滤

1.1 核心思想:

协同过滤是推荐系统中的经典算法,它通过用户与物品之间的交互行为来衡量物品或用户之间的相似性,而不考虑用户或物品的自身属性。这是符合生活经验的,例如对于两个年龄、性别、学历等硬性指标都完全一致的两人,他们的喜好可能千差万别;而对于这些硬性属性完全不同的两人,他们也可能会拥有相同的兴趣爱好。因此,希望推荐系统将前一种情况视为不同类用户,而将后一种情况视为相似用户,这就是协同过滤的基本思想。

1.2 相似性度量方法:

  • 余弦相似度 :

        sim(x,y) = \frac{x·y}{\sqrt{||x||^2||y||^2}}

  【 应用方法】:

  1. 建立用户-物品的交互矩阵A_{m×n},其中m为用户数量,n为物品数量,矩阵填充为用户与物品的交互情况。(例如,用0和1表示用户是否点赞该物品;用分数表示用户对物品的喜爱程度)
  2. 若要计算用户i与用户j之间的相似度,则提取出行向量x_{i},x_{j},计算二者的余弦相似度。注意!若采用字典的方式来进行存储,只需要遍历二者共同交互过的物品即可。这是因为,若用户没有交互过一个物品,则交互矩阵对应位置的元素为0,做内积计算之后的结果也为0。
  3. 若要计算物品k与物品p之间的相似度,则提取出列向量y_{p},y_{k},计算二者的余弦相似度。

  【 细节】:交互矩阵在实际场景下是很稀疏的,因此根据userCF/itemCF选择合适的字典进行存储。

  【使用场景】:余弦相似度广泛应用于文本相似度、用户相似度、文本相似度的度量。

UserCF:
{"user1":["item1","item2",..],
"user2":["item1","item2",..],
"user3":["item1","item2",..]}

ItemCF:
{"Item1":["user1","user2",...],
 "Item2":["user1","user2",...],
 "Item3":["user1","user2",...],
  
  • 皮尔逊相关系数:  sim(x,y) = \frac{x^{c}·y\frac{c}{}}{\sqrt{||x^{c}||^2||y^{c}||^2}}

【评价】:皮尔逊相关系数本质上仍为余弦相似度,但是增加了中心化处理。

【使用场景】:

  1. 可以用于度量两个变量的变化趋势是否一致。可以用于消除用户评分尺度差异的影响。例如用户1习惯打高分[3,4,5],用户2习惯打低分[1,2,3],虽然评分的绝对值相差较大,但是他们对于这三个物品的喜好程度趋势是相似的,采用皮尔逊系数可以更好地捕捉到这种特性。
  2. 不适合用于评估布尔值(0,1)之间的相关性。

UserCF

1.1 UserCF进行物品推荐流程:

  • 计算用户相似性矩阵
  • 给定用户ID,从用户相似性矩阵中找到与目标用户最相似的n个用户
  • 根据n个用户对候选物品的评分以及与目标用户的相似度,计算目标用户对候选物品的评分:
  • 将兴趣分数高的候选物品推荐给目标用户
  • 【目标用户→相似用户→物品评分】

1.2 代码实现:

# 创建用户与物品的交互矩阵
def loadData():
    users = {'Alice': {'A': 5, 'B': 3, 'C': 4, 'D': 4},
             'user1': {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3},
             'user2': {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5},
             'user3': {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4},
             'user4': {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1}
             }
    return users


# 计算用户相似性矩阵
user_matrix = loadData()  // 创建一个单位矩阵,对角线上的元素为1,其余元素均为0
u2u_sim = pd.DataFrame(
    np.identity(len(user_matrix)),
    index=user_matrix.keys(),
    columns=user_matrix.keys(),
)

for user1,item1 in user_matrix.items():
    for user2,item2 in user_matrix.items():
        if user1 == user2:
            continue
        common_keys = set(item1.keys())& set(item2.keys())  // 提取用户共同交互过的物品
        vec1=[]
        vec2=[]
        for key in common_keys:
            vec1.append(item1[key])
            vec2.append(item2[key])
        u2u_sim[user1][user2] = np.corrcoef(vec1,vec2)[0][1] //皮尔逊相关系数的返回结果是一个n×n的矩阵,n为向量长度,对角线元素为1,其余位置为相关性系数

----------------------------------------------------------------

Alice	user1	user2	user3	user4
Alice	1.000000	0.852803	0.707107	0.000000	-0.792118
user1	0.852803	1.000000	0.467707	0.489956	-0.900149
user2	0.707107	0.467707	1.000000	-0.161165	-0.466569
user3	0.000000	0.489956	-0.161165	1.000000	-0.641503
user4	-0.792118	-0.900149	-0.466569	-0.641503	1.000000
3.90659966456864
------------------------------------------------------------------
        
# 提取与Alice最相似的n个用户
target_user = 'Alice'
num = 2
sim_user = u2u_sim[target_user].sort_values(ascending=False)[1:num+1].index.tolist()
------------------
['user1', 'user2']
------------------


# 计算Alice对物品E的兴趣分数
weighted_scores = 0.
corr_values_sum = 0.
target_item = 'E'

for user in sim_user:
    weighted_scores += user_matrix[user][target_item]*u2u_sim[target_user][user]
    corr_values_sum += u2u_sim[target_user][user]
final_score = weighted_scores/corr_values_sum
----------------
3.90659966456864
----------------

1.3 UserCF进行召回流程:

  • 给定用户ID,从用户相似性矩阵中找到与该用户最相似的n个用户
  • 返回这n个用户最近交互过的k个物品
  • 召回nk个物品

ItemCF

1.1 ItemCF进行物品推荐流程:

  • 计算物品相似性矩阵
  • 给定候选物品,从物品相似性矩阵中找到最相似的n个物品
  • 根据目标用户对这n个物品的评分以及与物品间的相似度,计算目标用户对候选物品的评分
  • 【候选物品→物品相似度→物品评分】

1.2 ItemCF进行召回流程:

  • 获取用户ID,获取用户最近交互过的k个物品
  • 从物品相似度矩阵中获取与这k个物品最相似的n个物品
  • 召回nk个物品

1.3 ItemCF的物品相似度公式理解:

假设与物品i交互过的用户列表为[0,1,0,1],与物品j交互过的用户列表为[1,1,0,1],那么这两个向量的内积为:(0×1+1×1+0×0+1×1)=2,即同时交互过物品i和物品j的用户数量,等价于标准itemCF计算公式的分子。

1.4 标准itemCF的代码实现:

# 标准itemCF的物品相似性矩阵计算
from collections import defaultdict
import math

user_item_dict = {
    "user1": ["item1", "item2", "item3"],
    "user2": ["item1", "item3"]
}

# 统计每个物品被哪些用户喜欢
item_users = defaultdict(set)
for user, items in user_item_dict.items():
    for item in items:
        item_users[item].add(user)

# 计算物品之间的相似度
i2i_sim = defaultdict(dict)
for item, users in item_users.items():
    for other_item, other_users in item_users.items():
        if item == other_item:
            continue
        common_users = len(users.intersection(other_users))
        i2i_sim[item][other_item] = common_users / math.sqrt(len(users) * len(other_users))

print(i2i_sim)

1.5 ItemCF优化思路:

  1. 用户存在不同的给分偏好,有的用户倾向于给高分,有的用户倾向于给低分:事先对用户的评分进行归一化 / 皮尔逊相关系数
  2. 对活跃用户的惩罚:引入归一化因子,降低点击数量多的用户对物品相似度的增量。(天池新闻推荐大赛baseline所采用的方法)
    norm_factor = 1 / math.log(1 + user_click_count[user])
  3. 对热门物品的惩罚:
  4. 对活跃用户的惩罚:
  5. 有的物品虽然有大量的共现用户,但他们来自于同一个小圈子,因此对物品相似度的增量应当降低。 → Swing模型
  6. 关联规则+协同过滤:若还有其他因素会对物品相似度产生正向或负向的影响,则将其转换为>1或<1的权重,乘在物品的相似度增量上。(UserCF同理)

 Swing模型:

1.1 核心思想:

在itemCF中,若用户i和用户j同时喜欢物品a和物品b,即可认为这两件物品相似。但是如果用户i和用户j来自于同一个小圈子,他们所接触到的信息类似,因此喜欢的物品也大多相同,那么这些物品虽然有共同的受众,但是依然不能说明它们相似。反之,若用户i和用户j喜欢的物品基本不同,但是他们都喜欢物品c和物品d,那么很大程度上能说明物品c和物品d是相似的。

1.2 计算公式:

若两个用户喜欢的物品重叠越大,则他们对物品相似度的增量越小。

1.3 代码实现: 

from itertools import combinations

# 用户 - 物品交互字典,键为用户 ID,值为用户交互过的物品集合
u_items = {
    'user1': {'item1', 'item2', 'item3'},
    'user2': {'item1', 'item3', 'item4'},
    'user3': {'item2', 'item3', 'item5'},
    'user4': {'item1', 'item2', 'item5'}
}

# 物品 - 用户交互字典,键为物品 ID,值为与该物品有交互的用户集合
i_users = {
    'item1': {'user1', 'user2', 'user4'},
    'item2': {'user1', 'user3', 'user4'},
    'item3': {'user1', 'user2', 'user3'},
    'item4': {'user2'},
    'item5': {'user3', 'user4'}
}

# 定义 alpha 参数
alpha = 1

def swing_model(u_items, i_users):
    # print([i for i in i_users.values()][:5])
    # print([i for i in u_items.values()][:5])
    # 获取全排列组合对: [('item1', 'item2'), ('item1', 'item3'), ('item1', 'item4'), ('item1', 'item5'), ('item2', 'item3'), ('item2', 'item4'), ('item2', 'item5'), ('item3', 'item4'), ('item3', 'item5'), ('item4', 'item5')]
    item_pairs = list(combinations(i_users.keys(), 2)) 
    print("item pairs length:{}".format(len(item_pairs)))
    item_sim_dict = dict()
    for (i, j) in item_pairs:
        # combinations(i_users[i] & i_users[j], 2):此操作会从 i_users[i] 和 i_users[j] 的交集集合里选取元素,生成所有可能的包含 2 个元素的组合。例如,若交集集合为 {1, 2, 3},那么 combinations({1, 2, 3}, 2) 会生成 (1, 2)、(1, 3)、(2, 3) 这些组合。
        user_pairs = list(combinations(i_users[i] & i_users[j], 2)) #item_i和item_j对应的user取交集后全排列 得到user对
        result = 0
        for (u, v) in user_pairs:
            # list(u_items[u] & u_items[v]).__len__() 为用户1与用户2共同交互过的物品列表的长度
            result += 1 / (alpha + list(u_items[u] & u_items[v]).__len__()) #分数公式
        if result != 0 :
            item_sim_dict.setdefault(i, dict())
            item_sim_dict[i][j] = format(result, '.6f')
    return item_sim_dict 

 协同过滤算法小结:

1.1 算法缺陷:

  • 数据稀疏:当一个系统中用户和物品数量都很大时,用户交互过的物品只占很少的部分,导致用户-物品矩阵非常稀疏。→ 矩阵分解
  • 冷启动问题:新用户和新物品没有足够的行为数据,难以被系统准确推荐。→采用基于内容embedding的协同过滤。
  • 同质化推荐:热门物品能够获得与更多用户的交互,在相似度计算中更容易被推荐。 →对热门物品进行惩罚。
  • 实时性差、占用内存大:UserCF和ItemCF需要维护一个用户/物品相似性矩阵,若用户或物品的数量很大,则矩阵的维度成O(n^2)增长。且每一次用户-物品的交互行为更新后,都需要重新计算相似性矩阵,难以实时捕捉用户和物品的特征。→ 聚类方法,减少计算量 / 采用时间序列模型来捕获用户的兴趣变化。
  • 仅考虑了用户与物品的交互性,未考虑用户和物品的其他属性特征。→ 深度学习模型/ 特征交叉融合

1.2 思考:

  1. 什么时候使用UserCF和ItemCF?  答:UserCF适用于用户少、物品多、时效性强的场合。例如新闻推荐、新信息推荐;ItemCF适用于物品少、用户多、用户兴趣固定持久,物品更新速度慢的场合,例如艺术品、电影、音乐推荐。
  2. 协同过滤算法的缺点,如何改进?答:如上
  3. 相似度计算方法的对比?答:余弦相似度容易受到用户打分偏好的影响,而皮尔逊相似度对用户打分进行了中心化处理,可以消除打分偏好的影响,反映出用户间真实的喜好相似性。

矩阵分解:

1.1 基本思想:

将用户-物品的共现矩阵m×n,分解成用户矩阵m×k和物品矩阵k×n,其中k为隐向量的维度,是需要调节的参数,要根据数据量的大小,权衡计算量和精确度。

1.2 评分预测:

 1.3 矩阵分解的优缺点:

  • 优点:
    • 泛化能力强: 一定程度上解决了稀疏问题
    • 空间复杂度低: 由于用户和物品都用隐向量的形式存放, 少了用户和物品相似度矩阵, 空间复杂度由n^2降到了(n+m)∗f
    • 更好的扩展性和灵活性:矩阵分解的最终产物是用户和物品隐向量, 这个深度学习的embedding思想不谋而合, 因此矩阵分解的结果非常便于与其他特征进行组合和拼接, 并可以与深度学习无缝结合
  • 缺点:
    • 矩阵分解算法依然是只用到了评分矩阵, 没有考虑到用户特征,物品特征和上下文特征, 这使得矩阵分解丧失了利用很多有效信息的机会。
    • 同时在缺乏用户历史行为的时候, 无法进行有效的推荐。
    • 为了解决这个问题, 逻辑回归模型及后续的因子分解机模型, 凭借其天然的融合不同特征的能力, 逐渐在推荐系统领域得到了更广泛的应用。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐