1. 初识协同过滤
在推荐系统的众多算法中,基于内容的推荐与基于领域的推荐在实践中得到了最广泛的应用。
其中基于领域的算法又分为两大类,
一类是基于用户的协同过滤算法,这种算法从用户的兴趣相似出发,给用户推荐与其兴趣相似的其他用户喜欢的物品;
另一类是基于物品的协同过滤算法,这种算法更容易理解,就是直接给用户推荐和他之前喜欢的物品相似的物品。
协同过滤的核心思想:
通过计算物品或用户的之间的相似度,找出相似度最高的TopN个,作为推荐的结果;
2. 基于用户的协同过滤(User-based CF)
基于用户对物品的偏好找到邻居用户(相似用户),然后将邻居用户(相似用户)喜欢的东西推荐给当前用户。
主要分为两个步骤:
- 找到相似的用户
- 将相似用户喜欢的(但该用户A没有发现或没看过的)物品推荐给该用户A
2.1 计算两个用户相似度
2.1.1 杰卡德(Jaccard)相似系数
这个是衡量两个集合的相似度一种指标。两个用户 uu 和 vv 交互商品交集的数量占这两个用户交互商品并集的数量的比例,称为两个集合的杰卡德相似系数,用符号 simuvsimuv 表示,其中 N(u),N(v)N(u),N(v) 分别表示用户 uu 和用户 vv 交互商品的集合。
由于杰卡德相似系数一般无法反映具体用户的评分喜好信息, 所以常用来评估用户是否会对某商品进行打分, 而不是预估用户会对某商品打多少分。
2.1.2 余弦相似度
余弦相似度衡量了两个向量的夹角,夹角越小越相似。首先从集合的角度描述余弦相似度,相比于Jaccard公式来说就是分母有差异,不是两个用户交互商品的并集的数量,而是两个用户分别交互的商品数量的乘积,公式如下:
其中N (u) 表示用户 u 购买物品的数量, N (v) 表示用户 v 购买物品的数量, 表示用户α 和b 购买相同物品的数量。
从向量的角度进行描述,令矩阵 AA 为用户-商品交互矩阵(因为是TopN推荐并不需要用户对物品的评分,只需要知道用户对商品是否有交互就行),即矩阵的每一行表示一个用户对所有商品的交互情况,有交互的商品值为1没有交互的商品值为0,矩阵的列表示所有商品。若用户和商品数量分别为 m,nm,n 的话,交互矩阵 AA 就是一个 mm 行 nn 列的矩阵。此时用户的相似度可以表示为(其中 u⋅vu⋅v 指的是向量点积):
其中, 表示用户u和用户v的相似度;u 表示用户u喜欢的物品集合;v 表示用户v喜欢的物品集合;
上述用户-商品交互矩阵在现实情况下是非常的稀疏了,为了避免存储这么大的稀疏矩阵,在计算用户相似度的时候一般会采用集合的方式进行计算。
from sklearn.metrics.pairwise import cosine_similarity
i = [1, 0, 0, 0]
j = [1, 0.5, 0.5, 0]
cosine_similarity([i, j])
2.1.3 皮尔逊相关系数
皮尔逊相关系数的公式与余弦相似度的计算公式非常的类似,首先对于上述的余弦相似度的计算公式写成求和的形式,其中 rui,rvirui,rvi 分别表示用户 uu 和用户 vv 对商品 ii 是否有交互(或者具体的评分值):
下面是皮尔逊相关系数计算公式,其中 rui,rvirui,rvi 分别表示用户 uu 和用户 vv 对商品 ii 是否有交互(或者具体的评分值), ¯ru,¯rvr¯u,r¯v 分别表示用户 uu 和用户 vv 交互的所有商品交互数量或者具体评分的平均值。
所以相比余弦相似度,皮尔逊相关系数通过使用用户的平均分对各独立评分进行修正,减小了用户评分偏置的影响。具体实现, 我们也是可以调包, 这个计算方式很多, 下面是其中的一种:
from scipy.stats import pearsonr
i = [1, 0, 0, 0]
j = [1, 0.5, 0.5, 0]
pearsonr(i, j)
2.2 找到最相似的K个用户,进行评分预测
有了用户的相似数据,针对用户U 挑选K 个最相似的用户,把他们购买过的物品中, U未购买过的物品推荐给用户U 即可。如果有评分数据,可以针对这些物品进一步打分,打分的原理与基于物品的推荐原理类似,公式如下:
其中是N( i )物品 i 被购买的用户集合, S (u, k )是用户u 的相似用户集合,挑选最相似的用户k 个,将重合的用户u 在物品 i 上的得分乘以用户u 和u 的相似度,累加后得到用户u 对于物品 i 的得分。
2.3 实现逻辑
基于物品的协同过滤的原理是用户U 购买了A 物品,推荐给用户U 和A 相似的物品B 、C 、D 。而基于用户的协同过滤,是先计算用户U 与其他的用户的相似度,然后取和U 最相似的几个用户,把他们购买过的物品推荐给用户U。
具体用户-物品数据如下:
2.3.1 item倒排
为了计算用户相似度,我们首先要把用户购买过物品的索引数据转化成物品被用户购买过的索引数据,即物品的倒排索引;
如下图所示:
2.3.2 计算相似度
A | B | C | D | |
---|---|---|---|---|
A | ||||
B | ||||
C | ||||
D |
2.3.3 计算评分
假设,用户A的相似用户是B、C、D;
用户B、C、D之前浏览过的物品但是用户A没有浏览过的物品是c、e,所以用户A对物品c、e的评分:
2.3.4 代码实现
import time
import math
dataset = [('A', 'a', 5), ('A', 'b', 4), ('A', 'd', 3)
,('B', 'a', 4), ('B', 'c', 4)
,('C', 'b', 3), ('C', 'e', 4)
,('D', 'c', 5), ('D', 'd', 3), ('D', 'e', 4)
]
test = ['A']
def get_scores(dataset):
scores={}
for u,i,score in dataset:
scores[tuple([u,i])]=score
return scores
def score(scores,u,item):
s=scores[tuple([u,item])]
return s
def toDict(dataset):
train, test = [], []
for user, item, _ in dataset:
train.append((user, item))
trainDict=ToDict(train)
return trainDict
# 倒排
def Inverse(train):
item_users = {}
for user in train:
for item in train[user]:
if item not in item_users:
item_users[item] = []
item_users[item].append(user)
return item_users
#计算用户相似度矩阵 {u:(v,dict)}
def simCosUser(item_users) :
"""
item_users:商品-用户(倒排结果)
"""
# 计算用户相似度矩阵
sim = {} # 2个用户同时对商品的关注数量
num = {} # 存放用户对商品关注的数量
for item in item_users:
users = item_users[item]
for i in range(len(users)):
u = users[i]
if u not in num:
num[u] = 0
num[u] += 1 #计算每个用户关注item数量
if u not in sim:
sim[u] = {}
for j in range(len(users)):
if j == i:
continue
else:
v = users[j]
if v not in sim[u]:
sim[u][v] = 0 # 计算用户u和用户v关注item数量相同的商品数
sim[u][v] += 1
for u in sim:
for v in sim[u]:
sim[u][v] /= math.sqrt(num[u] * num[v]) # sim[u][v]用户u和用户v的相似度
# 按照相似度排序
sorted_user_sim = {k: list(sorted(v.items(), key=lambda x: x[1], reverse=True)) for k, v in sim.items()}
return sorted_user_sim
# 用户做推荐
def GetUserRecommendation(train,user,simUV,K,N,scores):
"""
train: 训练集数据
user: 给用户id推荐
simUV:用户间相似计算
K:选择距离最近的K个用户
N:推荐item的数量
"""
items = {}
seen_items = set(train[user])
topK=simUV[user][:K] # 用户user最相似的K个用户
for u,sim in topK:
for item in train[u]:
# 将用户过去没有看过的,但其相似用户看过的item,给用户推荐
if item not in seen_items: # 去掉用户已经看过的
if item not in items:
items[item] = 0
items[item] += sim*score(scores,u,item) # 相似用户对物品的评分 score(u,v)))]
recs = list(sorted(items.items(), key=lambda x: x[1], reverse=True))[:N]
return recs
K = 1
N = 10
train = toDict(dataset)
scores = get_scores(dataset)
print("-----------1.倒排序------------")
item_users=Inverse(train) # 商品-用户id
print("-----------2.计算相似度-----------")
simUV=simCosUser(item_users)
print(simUV)
print("-----------3.推荐的item-----------")
recs=RecFunc(train,test,simUV,K,N,scores)
print(recs)
-----------1.倒排序------------
-----------2.计算相似度-----------
{'A': [('B', 0.4082482904638631), ('C', 0.4082482904638631), ('D', 0.3333333333333333)], 'B': [('A', 0.4082482904638631), ('D', 0.4082482904638631)], 'D': [('B', 0.4082482904638631), ('C', 0.4082482904638631), ('A', 0.3333333333333333)], 'C': [('A', 0.4082482904638631), ('D', 0.4082482904638631)]}
-----------3.推荐的item-----------
{'A': [('c', 3.2996598285221186), ('e', 2.9663264951887856)]}
2.4 UserCF优缺点
User-based算法存在两个重大问题:
数据稀疏性。
一个大型的电子商务推荐系统一般有非常多的物品,用户可能买的其中不到1%的物品,不同用户之间买的物品重叠性较低,导致算法无法找到一个用户的邻居,即偏好相似的用户。这导致UserCF不适用于那些正反馈获取较困难的
应用场景(如酒店预订, 大件商品购买等低频应用)算法扩展性。
基于用户的协同过滤需要维护用户相似度矩阵以便快速的找出Topn相似用户, 该矩阵的存储开销非常大,存储空间随
着用户数量的增加而增加,不适合用户数据量大的情况使用。
3. 基于物品的协同过滤(Item-based CF)
基于物品的协同过滤算法的核心思想:给用户推荐那些和他们之前喜欢的物品相似的物品;
主要分为两个步骤:
第一步:计算两个物品用户相似度;
这里取的是余弦相似度:
其中,N(i) — 喜欢物品i的用户列表,N(j) —喜欢物品j的用户列表
第二步:找到最相似的k个物品,分别对k个物品评分;
其中,N(u)表示用户喜欢的物品列表,S(j,k)表示用户u的历史兴趣物品列表中物品 i 最相似的 k 个物品;
3.1 实现逻辑
计算相似度的方法同上面(User-based CF)介绍的那几种方法;
注意:
物品的相似度,要把物品A和物品B共有的相似物品C的相似度相加,算作物品C的相似度;
3.2 代码实现
import time
import math
dataset = [('A', 'a', 5), ('A', 'b', 4), ('A', 'd', 3)
,('B', 'a', 4), ('B', 'c', 4), ('E', 'e', 5)
,('C', 'c', 3), ('C', 'd', 4)
,('D', 'b', 5), ('D', 'c', 3), ('D', 'd', 4)
,('E', 'a', 5), ('E', 'd', 3)
]
test = ['A']
def get_scores(dataset):
scores={}
for u,i,score in dataset:
scores[tuple([u,i])]=score
return scores
def score(scores,u,item):
s=scores[tuple([u,item])]
return s
def toDict(dataset):
train, test = [], []
for user, item, _ in dataset:
train.append((user, item))
trainDict=ToDict(train)
return trainDict
def ToDict(data):
data_dict = {}
for user, item in data:
if user not in data_dict:
data_dict[user] = set()
data_dict[user].add(item)
data_dict = {k: list(data_dict[k]) for k in data_dict}
return data_dict
#计算商品相似度矩阵
def simCosItem(train) :
sim = {}
num = {}
for user, items in train.items():
for i in items:
if i not in num.keys():
num[i] = 0
num[i] += 1
for j in items:
if i == j: continue
if i not in sim.keys():
sim[i]={}
if j not in sim[i].keys():
sim[i][j] = 0
# 当用户同时购买了i和j
sim[i][j] += 1
item_sim = {}
for i, related_items in sim.items():
item_sim[i] = {}
for j, ij in related_items.items():
item_sim[i][j] = ij / math.sqrt(num[i] * num[j])
# 按照相似度排序
sorted_item_sim = {}
norm_item_sim = {}
for k, v in item_sim.items():
sorted_item_sim[k] = sorted(v.items(), key=lambda x: x[1], reverse=True)
itme_sim_max = sorted_item_sim[k][0][1]
norm_item_sim[k] = [(itemsim[0],itemsim[1]/itme_sim_max) for itemsim in sorted_item_sim[k]]
return norm_item_sim
# 推荐
def GetUserRecommendation(train,user,simIT,K,N,scores): # item=itme(test中的item)
"""
train: 训练集数据
item: 给item推荐
simIT:item间相似计算
K:选择距离最近的K个item
N:推荐item的数量
"""
rec_items = {}
seen_items = set(train[user])
for item_i in train[user]:
for item_j,sim in simIT[item_i][:K]:
# 测试集test的item要在训练集train出现过
if item_j not in seen_items:
if item_j not in rec_items:
rec_items[item_j] = 0
rec_items[item_j] += score(scores,user,item_i)*sim
recs = list(sorted(rec_items.items(), key=lambda x: x[1], reverse=True))[:N]
return recs
def RecFunc(train,test,simIT,K,N,scores):
"""
给测试集合的用户推荐,并记录推荐的结果存放在字典中
train: 训练集数据
test:测试集数据
user: 给用户id推荐
simIT:用户间相似计算
K:选择距离最近的K个item
N:推荐item的数量
"""
recs={}
for user in test:
recList=GetUserRecommendation(train,user,simIT,K,N,scores)
recs[user] = recList
return recs
K=1
N=10
train = toDict(dataset)
print("------------------------1、计算商品相似度--------------------------------")
simIT = simCosItem(train) #商品间相似度
print(simIT)
print("------------------------2、根据用户A喜欢的商品的商品相似度计算推荐商品----------------")
scores = get_scores(dataset)
recs=RecFunc(train,test,simIT,K,N,scores)
print(recs)
------------------------1、计算相似度--------------------------------
{'d': [('b', 1.0), ('a', 0.8164965809277263), ('c', 0.8164965809277263), ('e', 0.7071067811865476)], 'a': [('d', 1.0), ('e', 1.0), ('b', 0.7071067811865476), ('c', 0.5773502691896256)], 'b': [('d', 1.0), ('a', 0.5773502691896258), ('c', 0.5773502691896258)], 'c': [('d', 1.0), ('b', 0.7071067811865476), ('a', 0.5773502691896256)], 'e': [('a', 1.0), ('d', 0.8660254037844385)]}
------------------------2、根据商品相似度计算推荐商品----------------
{'A': [('c', 7.645642165489811), ('e', 7.121320343559643)]}
4. 总结
4.1 UserCF vs. ItemCF
基于用户协同(User-based CF) | 基于物品协同(Item-based CF) | |
---|---|---|
原理 | 用户喜欢那些和他有相似爱好的用户喜欢的东西 | 用户喜欢跟他过去喜欢的物品相似的物品 |
适用范围 | 适用于用户量较小的场合,如果用户很多,计算相似度代价高 | 适用于物品数量明显小于用户数量的场合,如果物品很多(新闻/视频),计算物品相似度代价高 |
优点 | 较其他算法新颖度更高对复杂的数据不感冒,视频/音频/图片 | 可以推荐出更有深度的内容对长尾商品相对更友好对复杂的数据不感冒,视频/音频/图片 |
缺点 | 用户冷启动问题(新用户没有行为)可解释性弱 | 物品冷启动(新物品行为少)可解释性比较UserCF强 |
4.2 基于用户协同和基于物品协同的优缺点
4.2.1 推荐的场景
UserCF :是利用用户间的相似性来推荐的,所以假如物品的数量远远超过用户的数量,那么可以考虑使用User CF ,
UserCF 是推荐用户所在兴趣小组中的热点,更注重社会化,
适用场景:新闻、博客或者微内容的推荐系统,
因为其内容更新频率非常高,特别是在社交网络中, UserCF可以增加用户对推荐解释的信服程度。
Item CF :是利用物品间的相似性来推荐的,所以假如用户的数量远远超过物品的数量,那么可以考虑使用Item CF ,
ItemCF 则是根据用户历史行为推荐相似物品,更注重个性化。
适用场景:比如购物网站,
因其物品的数据相对稳定,因此计算物品的相似度时不但计算量较小,而且不必频繁更新;
4.2.2 多样性(覆盖率)
覆盖率,指一个推荐系统能否给用户提供多种选择;
一般来说,Item CF 的多样性要远远好于UserCF ,因为UserCF 会更倾向于推荐热门的物品。
也就是说, ItemCF 的推荐有很好的新颖性,容易发现并推荐长尾里的物品。
所以大多数情况, ItemCF 的精度稍微小于UserCF ,但是如果考虑多样性, UserCF 却比ItemCF要好很多。
UserCF 经常推荐热门物品,所以它在推荐长尾里的项目方面的能力不足;而Item CF 只推荐A 领域给用户,这样它有限的推荐列表中就可能包含了一定数量的非热门的长尾物品。ItemCF 的推荐对单个用户而言,显然多样性不足,但是对整个系统而言,因为不同的用户的主要兴趣点不同,所以系统的覆盖率会比较好。
4.2.3 用户特点
用户特点对推荐算法影响的比较大。
对于UserCF ,推荐的原则是假设用户会喜欢那些和他有相同喜好的用户喜欢的东西,但是假如用户暂时找不到兴趣相投的邻居,那
么UserCF 的推荐效果就会大打折扣,因此用户是否适应UserCF 算法跟他有多少邻居是成正比关系的。
同理,对于ItemCF ,基于物品的协同过滤算法也是有一定前提的,即用户喜欢和他以前购买过的物品相同类型的物品,那么我们可以计算一个用户喜欢的物品的自相似度。一个用户喜欢物品的自相似度大,就说明他喜欢的东西都是比较相似的,即这个用户比较符合ItemCF 方法的基本假设,那么他对ItemCF 的适应度自然比较好, 反之,如果自相似度小,就说明这个用户的喜好习惯并不满足ItemCF 方法的基本假设, 那么用ItemCF 方法所做出的推荐对于这种用户来说,其推荐效果可能不是很好。
4.2.4 泛化能力差
协同过滤无法将两个物品相似的信息推广到其他物品的相似性上。 导致的问题是热门物品具有很强的头部效应, 容易跟大量物品产生相似, 而尾部物品由于特征向量稀疏, 导致很少被推荐。
j简单来说就是,两个不同领域的最热门物品之间往往具有比较高的相似度。这个时候,仅仅靠用户行为数据是不能解决这个问题的,因为用户的行为表示这种物品之间应该相似度很高。此时,我们只能依靠引入物品的内容数据解决这个问题,比如对不同领域的物品降低权重等。
4.2.5 协同过滤算法的权重改进
- 基础算法
图1为最简单的计算物品相关度的公式, 分子为同时喜好itemi和itemj的用户数 - 对热门物品的惩罚
图1存在一个问题, 如果 item-j 是很热门的商品,导致很多喜欢 item-i 的用户都喜欢 item-j,这时 wij 就会非常大。同样,几乎所有的物品都和 item-j 的相关度非常高,这显然是不合理的。所以图2中分母通过引入 N(j) 来对 item-j 的热度进行惩罚。如果物品很热门, 那么 N(j) 就会越大, 对应的权重就会变小。 - 对热门物品的进一步惩罚
如果 item-j 极度热门,上面的算法还是不够的。举个例子,《Harry Potter》非常火,买任何一本书的人都会购买它,即使通过图2的方法对它进行了惩罚,但是《Harry Potter》仍然会获得很高的相似度。这就是推荐系统领域著名的 Harry Potter Problem。
如果需要进一步对热门物品惩罚,可以继续修改公式为如图3所示,通过调节参数 α , α 越大,惩罚力度越大,热门物品的相似度越低,整体结果的平均热门程度越低。 - 对活跃用户的惩罚
同样的,Item-based CF 也需要考虑活跃用户(即一个活跃用户(专门做刷单)可能买了非常多的物品)的影响,活跃用户对物品相似度的贡献应该小于不活跃用户。图4为集合了该权重的算法。
4.2.6 综上小结
-
基于用户协同过滤
1)根据不同用户关注的同一个商品,去计算这些不同用户之间的相似度;
2)用户A的相似用户B对商品a的评分= 用户A与用户B之间的相似度 x 用户B对商品b的评分
-
基于物品协同过滤
1)根据同一个用户关注的不同商品,去计算这些不同商品之间的相似度;
2)用户A对商品a的相似商品b的评分 = 用户A对商品a的评分 x 商品a与商品b之间相似度
UserCF:适于用户少,但有较多相同兴趣, 物品多, 时效性较强的场合, 比如新闻推荐场景,
ItemCF:更适用于兴趣变化较为稳定的应用, 更接近于个性化的推荐, 适合物品少,用户多,用户兴趣固定持久, 物品更新速度不是太快的场合, 比如推荐艺术品, 音乐, 电影。
4.3 其他知识点;
4.3.1 相似度计算方法的优缺点
cosine相似度还是比较常用的, 一般效果也不会太差, 但是对于评分数据不规范的时候, 也就是说, 存在有的用户喜欢打高分, 有的用户喜欢打低分情况的时候,有的用户喜欢乱打分的情况, 这时候consine相似度算出来的结果可能就不是那么准确了, 比如下面这种情况:
这时候, 如果用余弦相似度进行计算, 会发现用户d和用户f比较相似, 而实际上, 如果看这个商品喜好的一个趋势的话, 其实d和e比较相近, 只不过e比较喜欢打低分, d比较喜欢打高分。 所以对于这种用户评分偏置的情况, 余弦相似度就不是那么好了, 可以考虑使用下面的皮尔逊相关系数。
4.3.2 协同过滤的缺点及解决思路
协同过滤的特点就是完全没有利用到物品本身或者是用户自身的属性, 仅仅利用了用户与物品的交互信息就可以实现推荐,比较简单高效, 但这也是它的一个短板所在, 由于无法有效的引入用户年龄, 性别,商品描述,商品分类,当前时间,地点等一系列用户特征、物品特征和上下文特征, 这就造成了有效信息的遗漏,不能充分利用其它特征数据。
为了解决这个问题, 在推荐模型中引用更多的特征,推荐系统慢慢的从以协同过滤为核心到了以逻辑回归模型为核心, 提出了能够综合不同类型特征的机器学习模型。
参考资料:
《推荐系统实践》
《推荐系统与深度学习》