基于物品的协同算法
【核心思想】:给用户推荐那些和他们之前喜欢的物品相似的物品。
基于物品的协同算法首先计算物品之间的相似度,计算相似度的方法:
- 基于共同喜欢物品的用户列表计算;
- 基于余弦(Cosine-based)的相似度计算;
- 热门物品的惩罚;
基于共同喜欢物品的用户列表计算
【计算公式】:
w
i
j
=
∣
N
(
i
)
⋂
N
(
j
)
∣
∣
N
(
i
)
∣
∗
∣
N
(
j
)
∣
w_{ij} = \frac{|N(i) \bigcap N(j)|}{\sqrt{|N(i)| * |N(j)|}}
wij=∣N(i)∣∗∣N(j)∣∣N(i)⋂N(j)∣
其中,|N(i)| 和 |N(j)| 分别是购买物品 i 以及购买物品 j 的用户数,
∣
N
(
i
)
⋂
N
(
j
)
∣
|N(i) \bigcap N(j)|
∣N(i)⋂N(j)∣ 是同时购买物品 i 和物品 j 的用户数。
公式的核心是计算同时购买这两个物品的人数比例。当同时购买这两个物品人数越多,他们的相似度也就越高。那么我们为什么还要除以 ∣ N ( i ) ∣ ∗ ∣ N ( j ) ∣ \sqrt{|N(i)| * |N(j)|} ∣N(i)∣∗∣N(j)∣?
考虑这样一种情形,我们现在有 A、B、C 三种商品 以及 100 条购物记录,其中 A 单独购买 50 次,B 单独购买 30 次,C 单独狗欧迈 5 次,AB 共同购买 10 次,AC 共同购买 5 次。
商品 | 购物次数 |
---|---|
A | 50 |
B | 30 |
C | 5 |
AB | 10 |
AC | 5 |
如果我们仅仅计算购买两个物品的人数,那么 AB 的相似度大于 AC。但真的是这样的吗?观察上表我们可以发现,A、B 商品同时购买的次数占 B 商品购买总次数的比例仅仅为 25%,而 A、C 商品同时购买的次数占 C 商品购买总次数的 50%。从这个角度来说,A 商品与 C 商品更为相似。之所以同时购买 A、B 的次数高,是因为 B 商品比 C 商品更热门,使得 B 商品的购买次数更多。因此,我们用 ∣ N ( i ) ∣ ∗ ∣ N ( j ) ∣ \sqrt{|N(i)| * |N(j)|} ∣N(i)∣∗∣N(j)∣ 作为惩罚项,来降低商品之间的相似分数。
有了购买记录之后,我们要如何计算两两物品之间的相似度呢?我们可以构造一个 N x N 的矩阵来存储物品两两同时被购买的次数。遍历每个用户的购买历史,当 i 和 j 两个物品同时被购买时,在矩阵 (i, j) 位置上加 1。当遍历完成时,就可以得到共现次数矩阵。
商品 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|
1 | 2 | 1 | 1 | ||
2 | 2 | 1 | 2 | 1 | |
3 | 1 | ||||
4 | 1 | 2 | |||
5 | 1 | 1 |
在上表中,我们可以得到任意两个物品共同被购买的次数。同时,观察上表我们也能发现矩阵是关于主对角线对称。
综上我们可以归纳出基于共同喜欢物品的用户列表计算的实现步骤:
- 根据购物记录计算两两物品的共现次数,并构建共现矩阵;
- 根据购物记录统计每个物品的出现次数;
- 套用公式,计算两两物品之间的相似度,并保存到相似度矩阵中。
【数据集以及包引入】:
import numpy as np
dataset = np.array([
[1, 2, 4],
[1, 4],
[1, 2, 5],
[2, 3],
[3, 5],
[2, 4]
])
【代码实现】:构建共现矩阵
def get_matrix(records):
# 获取共有多少种不同的商品
goods = []
for record in records:
for good in record:
if good not in goods:
goods.append(good)
# 对商品进行排序
goods.sort()
# 初始化共现矩阵
goods_num = len(goods)
matrix = np.mat(np.zeros((goods_num, goods_num)))
# 建立商品与矩阵坐标的映射
good_dict, index = {}, 0
for good in goods:
good_dict[good] = index
index += 1
# 填充共现矩阵
for record in records:
goods_length = len(record)
for i in range(goods_length - 1):
good_A_ind = good_dict[record[i]]
for j in range(i + 1, goods_length):
good_B_ind = good_dict[record[j]]
matrix[good_A_ind, good_B_ind] += 1
matrix[good_B_ind, good_A_ind] += 1
return matrix, good_dict
- 首先,获取商品的种类。
goods = []
for record in records:
for good in record:
if good not in goods:
goods.append(good)
- 接着,对商品进行排序并初始化共现矩阵。
goods.sort()
goods_num = len(goods)
matrix = np.mat(np.zeros((goods_num, goods_num)))
- 然后,建立商品与矩阵坐标的映射。
good_dict, index = {}, 0
for good in goods:
good_dict[good] = index
index += 1
- 最后,统计两两物品的共现次数,并填充到共现矩阵的指定位置处。
for record in records:
goods_length = len(record)
for i in range(goods_length - 1):
good_A_ind = good_dict[record[i]]
for j in range(i + 1, goods_length):
good_B_ind = good_dict[record[j]]
matrix[good_A_ind, good_B_ind] += 1
matrix[good_B_ind, good_A_ind] += 1
return matrix, good_dict
【代码实现】:统计商品出现次数
def get_good_count(records):
# 获取共有多少种不同的商品
goods = []
for record in records:
for good in record:
if good not in goods:
goods.append(good)
# 对商品进行排序
goods.sort()
# 初始化商品购买次数字典
good_count_dict = {}
for good in goods:
good_count_dict[good] = 0
# 开始统计商品的购买次数
for record in records:
for good in record:
good_count_dict[good] += 1
return good_count_dict
- 首先,获取商品的种类。
goods = []
for record in records:
for good in record:
if good not in goods:
goods.append(good)
- 接着,对商品进行排序,并初始化商品购买次数字典(用以记录每个商品的出现次数)。
goods.sort()
good_count_dict = {}
for good in goods:
good_count_dict[good] = 0
- 最后,统计商品的出现次数,并返回商品购买次数字典。
for record in records:
for good in record:
good_count_dict[good] += 1
return good_count_dict
【代码实现】:计算两两物品之间的相似度
def good_similarity(occu_matrix, good_dict, good_count_dict):
# 获取商品种类以及初始化相似度矩阵
goods = list(good_count_dict.keys())
goods_num = len(good_count_dict)
simi_matrix = np.mat(np.zeros((goods_num, goods_num)))
# 开始计算商品之间的相似度
for i in range(goods_num - 1):
good_A = goods[i]
good_A_ind = good_dict[good_A]
for j in range(i + 1, goods_num):
good_B = goods[j]
good_B_ind = good_dict[good_B]
# 计算相似度
similarity = occu_matrix[good_A_ind, good_B_ind] / (np.sqrt(good_count_dict[good_A] * good_count_dict[good_B]))
simi_matrix[good_A_ind, good_B_ind] = similarity
simi_matrix[good_B_ind, good_A_ind] = similarity
return simi_matrix
- 首先,获取商品种类以及初始化相似度矩阵。
goods = list(good_count_dict.keys())
goods_num = len(good_count_dict)
simi_matrix = np.mat(np.zeros((goods_num, goods_num)))
- 接着,计算两两商品之间的相似度。
for i in range(goods_num - 1):
good_A = goods[i]
good_A_ind = good_dict[good_A]
for j in range(i + 1, goods_num):
good_B = goods[j]
good_B_ind = good_dict[good_B]
# 计算相似度
similarity = occu_matrix[good_A_ind, good_B_ind] / (np.sqrt(good_count_dict[good_A] * good_count_dict[good_B]))
simi_matrix[good_A_ind, good_B_ind] = similarity
simi_matrix[good_B_ind, good_A_ind] = similarity
先前,我们用 ∣ N ( i ) ∣ ∗ ∣ N ( j ) ∣ \sqrt{|N(i)| * |N(j)|} ∣N(i)∣∗∣N(j)∣ 作为惩罚项,来降低商品之间的相似分数。但是,当物品 i 被更多人购买时,分子中的 N ( i ) ⋂ N ( j ) N(i) \bigcap N(j) N(i)⋂N(j) 和分母中的 N ( i ) N(i) N(i) 都会增长。对于热门物品来说,分子分母都加 1,整个值仍然在增长,这就会使得物品 i 和很多其他的物品相似度都偏高,这就是物品热门问题。
推荐结果过于热门,会使得个性化感知下降。以歌曲的相似为例,大部分用户都会在歌单中收藏《告白气球》这些热门歌曲,从而导致《告白气球》出现在很多不同类型歌单的推荐中。为了解决这个问题,我们需要对热门物品 i 进行惩罚。
【惩罚公式】:
w
i
j
=
∣
N
(
i
)
⋂
N
(
j
)
∣
∣
N
(
i
)
∣
α
∗
∣
N
(
j
)
∣
1
−
α
w_{ij} = \frac{|N(i) \bigcap N(j)|}{|N(i)|^\alpha * |N(j)|^{1-\alpha}}
wij=∣N(i)∣α∗∣N(j)∣1−α∣N(i)⋂N(j)∣
对比先前的公式可以发现,我们把根号去除,然后在 N(i) 和 N(j) 加上一个指数。实际上根号相当于 N(i) 和 N(j) 分别加上指数 0.5。这样的话,我们可以通过修改物品的指数来实现惩罚程度的调整。
一般来说,N(i) 和 N(j) 的值大于等于 1,我们来看下式:
>>> 2**0.5 - 2**0.4
0.09470565160020095
>>> 3**0.5 - 3**0.4
0.18020523365351737
假设 N(i) 的值较大,且 N(i) 的值越大,则给予 N(i) 的指数越大,则惩罚力度也越大。怎么理解呢?我们可以看到 3 的 0.5 次方比 3 的 0.4 次方大,且差值比 2 的 0.5 次方与 0.4 次方的差值大得多,这说明 N(i) 越大,且次方越高,它的增长速度更快,从而使得分母的增速快于分子,使得整体相似度取值降低。
基于上述理论,我们再把 good_similarity() 函数略作修改。首先,新增一个参数 alpha,其取值范围限定在 (0, 0.5)。
def good_similarity2(occu_matrix, good_dict, good_count_dict, alpha=0.3):
然后在计算相似度部分,先判断哪一个商品更热门,并给予它较大的 alpha 值。
# 计算相似度
good_A_count, good_B_count = good_count_dict[good_A], good_count_dict[good_B]
if good_A_count > good_B_count:
similarity = occu_matrix[good_A_ind, good_B_ind] / (good_A_count**(1 - alpha) * good_B_count**alpha)
else:
similarity = occu_matrix[good_A_ind, good_B_ind] / (good_A_count**alpha * good_B_count**(1 - alpha))
至此,我们已经了解并用代码实现了基于共同喜欢物品的用户列表计算。
基于余弦的相似度计算
上一个方法非常简单,只需要统计物品的购买次数即可,但也存在缺陷——用户购买但并不喜欢。所以如果数据集中包含具体的评分数据,我们可以进一步把用户评分引入到相似度计算中。此时,我们可用余弦公式计算两个物品之间的相似度。
【计算公式】:
c
o
s
θ
=
N
i
⋅
N
j
∣
∣
N
i
∣
∣
∣
∣
N
j
∣
∣
=
∑
k
=
1
m
(
n
k
i
×
n
k
j
)
∑
k
=
1
m
n
k
i
2
×
∑
k
=
1
m
n
k
j
2
cos\theta = \frac{N_i \cdot N_j}{||N_i||||N_j||} = \frac{\sum_{k=1}^m(n_{ki} \times n_{kj})}{\sqrt{\sum_{k=1}^m n_{ki}^2} \times \sqrt{\sum_{k=1}^m n_{kj}^2}}
cosθ=∣∣Ni∣∣∣∣Nj∣∣Ni⋅Nj=∑k=1mnki2×∑k=1mnkj2∑k=1m(nki×nkj)
其中,
n
k
i
n_{ki}
nki 是用户 k 对物品 i 的评分,如果没有评分则为 0。因此,基于余弦的相似度计算需要拥有大量的评分数据。
首先,我们需要对数据集略作调整,为每个商品购买记录添加评分。因此,需要将原本列表中的元素修改为字典。
dataset = np.array([
[{1: 5}, {2: 4}, {4: 1}],
[{1: 4}, {4: 3}],
[{1: 5}, {2: 3}, {5: 3}],
[{2: 3}, {3: 5}],
[{3: 4}, {5: 2}],
[{2: 4}, {4: 3}]
])
接着,我们再把之前编写的函数进行修改,以满足数据集变动的要求。
【get_matrix()】:修改获取商品种类以及填充共现矩阵的代码。此时,共现矩阵不再是统计物品共同出现的次数,而是两个商品的评分乘积。
def get_matrix(records):
# 获取共有多少种不同的商品
goods = []
for record in records:
for good_info in record:
good = list(good_info)[0]
if good not in goods:
goods.append(good)
# 对商品进行排序
goods.sort()
# 初始化共现矩阵
goods_num = len(goods)
matrix = np.mat(np.zeros((goods_num, goods_num)))
# 建立商品与矩阵坐标的映射
good_dict, index = {}, 0
for good in goods:
good_dict[good] = index
index += 1
# 填充共现矩阵
for record in records:
goods_length = len(record)
for i in range(goods_length - 1):
good_A_info = record[i]
good_A_ind, good_A_grade = good_dict[list(good_A_info.keys())[0]], list(good_A_info.values())[0]
for j in range(i + 1, goods_length):
good_B_info = record[j]
good_B_ind, good_B_grade = good_dict[list(good_B_info.keys())[0]], list(good_B_info.values())[0]
matrix[good_A_ind, good_B_ind] += good_A_grade * good_B_grade
matrix[good_B_ind, good_A_ind] += good_A_grade * good_B_grade
return matrix, good_dict
【get_good_grade()】:修改获取商品种类数以及计算商品的评分。
def get_good_grade(records):
# 获取共有多少种不同的商品
goods = []
for record in records:
for good_info in record:
good = list(good_info)[0]
if good not in goods:
goods.append(good)
# 对商品进行排序
goods.sort()
# 初始化商品评分字典以及商品购买次数字典
good_grade_dict = {}
good_count_dict = {}
for good in goods:
good_grade_dict[good] = 0
good_count_dict[good] = 0
# 开始统计商品的评分
for record in records:
for good_info in record:
good, grade = list(good_info.keys())[0], list(good_info.values())[0]
good_grade_dict[good] += grade ** 2
good_count_dict[good] += 1
return good_grade_dict
good_similarity() 函数可以不作修改。
在得到物品之间的相似度后,我们可以计算用户 u 对一个物品 i 的预测分数。
【计算公式】:
P
u
i
=
∑
N
(
u
)
⋂
S
(
j
,
k
)
w
j
i
s
c
o
r
e
u
i
P_{ui} = \sum_{N(u)\bigcap S(j,k)} w_{ji}score_{ui}
Pui=N(u)⋂S(j,k)∑wjiscoreui
其中,S(j,k) 是物品 j 相似物品的集合,一般来说 j 的相似物品集合是相似分数最高的 k 个,然后我们求相似物品集合与用户评分集合的交集。
s
c
o
r
e
u
i
score_{ui}
scoreui 是用户对已购买的物品 i 的评分,如果没有评分数据,则取 1。
如果待打分的物品和用户购买过的多个物品相似,则将相似分数相加,相加后的得分越高,则用户购买可能性越大。比如用户购买过《明朝那些事儿》(评分 0.8)和《品三国》(评分 0.6),而《鱼羊野史》分别和《明朝那些事儿》、《品三国》的相似分数为 0.2 和 0.1,则用户在《鱼羊野史》上的分数则为 0.22 分(0.8 * 0.2 + 0.6 * 0.1)。这时候找出与用户喜欢的物品相似度最高的 K 个物品。
【代码实现】:
def recommand(simi_matrix, good_dict, user_records, num=3):
purchased_goods = list(user_records.keys())
goods_grade = {}
for good in good_dict:
goods_grade[good] = 0
for good in good_dict:
# 对于已购买的物品不再进行推荐
if good not in user_records:
# 获取当前物品的其他相似度最高的 num 个物品
good_ind = good_dict[good]
for good_i in good_dict:
if good_i in purchased_goods:
goods_grade[good] += user_records[good_i] * simi_matrix[good_ind, good_dict[good_i]]
else:
goods_grade[good] += 1 * simi_matrix[good_ind, good_dict[good_i]]
top_simi_goods = list(goods_grade.items())
top_simi_goods.sort(key=lambda x:x[1], reverse=True)
return top_simi_goods[:num]
- 首先,获取已购买物品列表以及初始化物品评分字典。
purchased_goods = list(user_records.keys())
goods_grade = {}
for good in good_dict:
goods_grade[good] = 0
- 遍历所有物品列表,对于已购买的物品不再进行推荐。对未购买的物品则根据先前的公式进行计算。
# 遍历所有物品列表
for good in good_dict:
# 对于已购买的物品不再进行推荐
if good not in user_records:
good_ind = good_dict[good]
for good_i in good_dict:
if good_i in purchased_goods:
goods_grade[good] += user_records[good_i] * simi_matrix[good_ind, good_dict[good_i]]
else:
goods_grade[good] += 1 * simi_matrix[good_ind, good_dict[good_i]]
- 最后,获取评分最高的 num 个物品,并返回。
top_simi_goods = list(goods_grade.items())
top_simi_goods.sort(key=lambda x:x[1], reverse=True)
return top_simi_goods[:num]
在上面的计算过程中,还有一个重要的参数 num,即对当前物品的相似物品中评分最高的 num 个物品进行召回。
- num 值过大,会召回很多相关性不强的物品,导致准确性下降;
- num 值过小,召回的物品过少,使得准确率也不高。
一般来说,算法工作人员需要尝试不同的 num 值对比算法准确率和召回率,以便选择最佳的 num 值。
基于用户的协同算法
基于用户的协同过滤(User CF)的原理其实和基于物品的协同过滤类似,所不同的是:
- 基于物品的协同过滤:用户 U 购买 A 物品,推荐给用户 U 和 A 相似的物品 B、C、D。
- 基于用户的协同过滤:先计算用户 U 与其他的用户的相似度,然后取和 U 最相似的几个用户,把他们购买过的物品推荐给用户 U。
为了计算用户相似度,我们首先要把用户购买过物品的索引数据(在前面所讲的基于物品的协同算法中使用)转化成物品被用户购买过的索引数据,即物品的倒排索引。
用户 | 购买记录 |
---|---|
A | 1, 2, 4 |
B | 2, 4 |
C | 1, 2, 5 |
D | 2, 3 |
上表是用户购买物品的索引数据,我们将其转换为物品被用户购买的索引数据。
购买记录 | 用户 |
---|---|
1 | A, C |
2 | A, B, C |
3 | D |
4 | A, B |
5 | C |
【数据集及所需包】:
import numpy as np
dataset = np.array([
{'user': 'A', 'record': [1, 2, 4]},
{'user': 'B', 'record': [2, 4]},
{'user': 'C', 'record': [1, 2, 5]},
{'user': 'D', 'record': [2, 3]}
])
【代码实现】:建立物品倒排索引。
def convert2Items(dataset):
items_dict = {}
users_list = []
for data in dataset:
user, records = data['user'], data['record']
users_list.append(user)
for record in records:
if record not in items_dict:
items_dict[record] = []
items_dict[record].append(user)
return items_dict, sorted(users_list)
- 首先,创建 items_dict 字典,用以存储物品索引,users_list 列表用以存储用户信息。
- 接着,读取数据集中的每一行数据,获取其中的用户以及购物记录,并将用户添加到 users_list 列表中。
for data in dataset:
user, records = data['user'], data['record']
users_list.append(user)
// ...
- 然后,在上一步的循环内,我们接着遍历购物记录,若当前物品不在 items_dict 字典中,则作为字典的 key;若存在,则将购买当前物品的用户添加到字典中。
for record in records:
if record not in items_dict:
items_dict[record] = []
items_dict[record].append(user)
- 最后,将物品索引 items_dict 以及排序后的用户信息列表 users_list 返回。
建立好物品的倒排索引后,就可以根据相似度公式计算用户之间的相似度。
w
a
,
b
=
∣
N
(
a
)
⋂
N
(
b
)
∣
∣
N
(
a
)
∣
×
∣
N
(
b
)
∣
w_{a,b} = \frac{|N(a) \bigcap N(b)|}{\sqrt{|N(a)| \times |N(b)|}}
wa,b=∣N(a)∣×∣N(b)∣∣N(a)⋂N(b)∣
其中,N(a)、N(b) 分别表示用户 a、b 购买物品的数量,
∣
N
(
a
)
⋂
N
(
b
)
∣
|N(a) \bigcap N(b)|
∣N(a)⋂N(b)∣ 表示用户 a 和 b 购买相同物品的数量。
【代码实现】:根据物品索引数据生成用户矩阵。
def generate_user_matrix(items_dict, users_list):
user_num = len(users_list)
# 初始化用户矩阵
user_matrix = np.mat(np.zeros((user_num, user_num)))
# 建立用户与下标的对应关系
user_dict, ind = {}, 0
for user in users_list:
user_dict[user] = ind
user_dict[ind] = user
ind += 1
# 构建用户矩阵
for item in items_dict:
user_list = items_dict[item]
user_list_length = len(user_list)
for i in range(user_list_length - 1):
user_A_ind = user_dict[user_list[i]]
for j in range(i + 1, user_list_length):
user_B_ind = user_dict[user_list[j]]
user_matrix[user_A_ind, user_B_ind] += 1
user_matrix[user_B_ind, user_A_ind] += 1
return user_matrix, user_dict
- 首先,获取用户的数目以及初始化用户矩阵。
user_num = len(users_list)
# 初始化用户矩阵
user_matrix = np.mat(np.zeros((user_num, user_num)))
- 接着,建立用户与下标的对应关系。
user_dict, ind = {}, 0
for user in users_list:
user_dict[user] = ind
user_dict[ind] = user
ind += 1
- 然后,构建用户矩阵。构建过程中,我们先从物品索引中获取指定物品的用户列表,然后依次统计用户的共现次数,最后将共现次数累加到用户矩阵的指定位置。
for item in items_dict:
user_list = items_dict[item]
user_list_length = len(user_list)
for i in range(user_list_length - 1):
user_A_ind = user_dict[user_list[i]]
for j in range(i + 1, user_list_length):
user_B_ind = user_dict[user_list[j]]
user_matrix[user_A_ind, user_B_ind] += 1
user_matrix[user_B_ind, user_A_ind] += 1
- 最后,将相似矩阵 simi_matrix 返回。
【代码实现】:统计每个用户的购物商品数。
def get_user_purchased_count(records):
user_purchased_dict = {}
for record in records:
user_purchased_dict[record['user']] = len(record['record'])
return user_purchased_dict
【代码实现】:计算每对用户之间的相似度。
def user_similarity(user_matrix, user_dict, user_purchased_dict):
user_num = len(user_purchased_dict)
# 初始化用户相似度矩阵
simi_matrix = np.mat(np.zeros((user_num, user_num)))
# 计算每对用户的相似度
for user_A, user_A_num in user_purchased_dict.items():
user_A_ind = user_dict[user_A]
other_users = list(user_purchased_dict.keys())
for user_B in other_users:
user_B_ind = user_dict[user_B]
similarity = user_matrix[user_A_ind, user_B_ind] / np.sqrt(user_purchased_dict[user_A] * user_purchased_dict[user_B])
simi_matrix[user_A_ind, user_B_ind] = similarity
simi_matrix[user_B_ind, user_A_ind] = similarity
return simi_matrix
- 首先,获取用户的数目以及初始化相似度矩阵。
user_num = len(user_purchased_dict)
# 初始化用户相似度矩阵
simi_matrix = np.mat(np.zeros((user_num, user_num)))
- 最后,计算每对用户的相似度。计算过程中,我们需要对用户之间两两进行组合,然后依次计算每对用户之间的相似度,并填充到相似度矩阵的对应位置处。
for user_A, user_A_num in user_purchased_dict.items():
user_A_ind = user_dict[user_A]
other_users = list(user_purchased_dict.keys())
for user_B in other_users:
user_B_ind = user_dict[user_B]
similarity = user_matrix[user_A_ind, user_B_ind] / np.sqrt(user_purchased_dict[user_A] * user_purchased_dict[user_B])
simi_matrix[user_A_ind, user_B_ind] = similarity
simi_matrix[user_B_ind, user_A_ind] = similarity
return simi_matrix
【测试代码】:
>>> items_dict, users_list = convert2Items(dataset)
>>> items_dict
{1: ['A', 'C'], 2: ['A', 'B', 'C', 'D'], 4: ['A', 'B'], 5: ['C'], 3: ['D']}
>>> users_list
['A', 'B', 'C', 'D']
>>> user_purchased_dict = get_user_purchased_count(dataset)
>>> user_purchased_dict
{'A': 3, 'B': 2, 'C': 3, 'D': 2}
>>> user_matrix, user_dict = generate_user_matrix(items_dict, users_list)
>>> user_matrix
[[0. 2. 2. 1.]
[2. 0. 1. 1.]
[2. 1. 0. 1.]
[1. 1. 1. 0.]]
>>> user_dict
{'A': 0, 0: 'A', 'B': 1, 1: 'B', 'C': 2, 2: 'C', 'D': 3, 3: 'D'}
>>> simi_matrix = user_similarity(user_matrix, user_dict, user_purchased_dict)
>>> simi_matrix
matrix([[0. , 0.81649658, 0.66666667, 0.40824829],
[0.81649658, 0. , 0.40824829, 0.5 ],
[0.66666667, 0.40824829, 0. , 0.40824829],
[0.40824829, 0.5 , 0.40824829, 0. ]])
有了用户的相似数据,我们就可以针对用户 U 挑选指定个数的最相似用户,把他们购买过但用户 U 没有购买过的物品推荐给用户 U。
如果有评分数据,则可以针对这些物品进一步打分:
P
u
i
=
∑
N
(
i
)
⋂
S
(
u
,
k
)
w
v
u
s
c
o
r
e
v
u
P_{ui} = \sum_{N(i) \bigcap S(u, k)} w_{vu}score_{vu}
Pui=N(i)⋂S(u,k)∑wvuscorevu
其中,N(i) 是物品 i 被购买的用户集合,S(u,k) 是用户 u 的相似用户集合,挑选最相似的用户 k 个,将这些最相似用户 v 在物品 i 上的得分乘以用户 u 与 v 的相似度,累加后即可得到用户 u 对物品 i 的得分。
沿用先前的例子,假设我们现在要计算用户 C 对物品 4 的得分。
- 首先,我们需要获取 N(2)。通过查阅物品索引可以很快获得 N(4)。
4: ['A', 'B']
- 通过直接读取物品索引,我们可以得知用户 A 和用户 B 购物过物品 4。接着,我们再借助用户相似度矩阵来获取与用户 C 最相似的用户。这里我们假设 k 为 2。
用户 | A | B | C | D |
---|---|---|---|---|
A | 0 | 0.81649658 | 0.66666667 | 0.40824829 |
B | 0.81649658 | 0 | 0.40824829 | 0.5 |
C | 0.66666667 | 0.40824829 | 0 | 0.40824829 |
D | 0.40824829 | 0.5 | 0.40824829 | 0 |
- 通过用户相似度矩阵,我们可以直接读取 C 行,然后将读取的数据排个序,取出其中最大的 2 个用户即可。在这里我们发现用户 B 和用户 D 与用户 C 的相似度相同,那么我们随机获取一个即可,我们选择 B。N(4) 与 S(C, 2) 的交集仍然是 (A, B)。
- 假设 A 对物品 4 的评分是 4 分,B 对物品 4 的评分是 2 分,那么最后的得分为:
P A 4 = 0.667 × 4 + 0.408 × 2 = 3.484 P_{A4} = 0.667 \times 4 + 0.408 \times 2 = 3.484 PA4=0.667×4+0.408×2=3.484
在了解如何计算用户对物品的得分后,我们来实现相应功能的代码。
【数据集】:
records = np.array([1, 2, 3])
user_score = {
'A': {1: 4, 2: 3, 4: 4},
'B': {2: 3, 4: 5},
'C': {1: 5, 2: 3, 5: 3},
'D': {2: 4, 3: 2}
}
实际上用户对商品的评分需要到数据库进行查询,但在这里为了说明实现过程,则用字典的形式呈现用户评分数据。
【代码实现】:获取当前用户最相似的指定数量的用户列表。
def get_user_similarity(simi_matrix, user, user_num, user_dict):
simi_user_list = simi_matrix[user_dict[user]]
user_ind_list = np.argsort(simi_user_list).tolist()[0][-user_num:]
user_list = []
for user_ind in user_ind_list:
user_list.append(user_dict[user_ind])
return user_list
- 首先,获取当前用户与其他用户的相似度数据。
simi_user_list = simi_matrix[user_dict[user]]
- 然后,对相似度数据进行排序,从中挑选相似度最高的 user_num 个用户。
user_ind_list = np.argsort(simi_user_list).tolist()[0][-user_num:]
user_list = []
for user_ind in user_ind_list:
user_list.append(user_dict[user_ind])
- 最后,返回相似用户列表。
【代码实现】:计算当前用户对各物品的得分。
def cal_score(simi_matrix, items_dict, user, user_num, user_dict, records, user_score):
simi_user_list = set(get_user_similarity(simi_matrix, user, user_num, user_dict))
score_dict = {}
for good in records:
user_list = set(items_dict[good])
user_set = simi_user_list & user_list
if good not in score_dict:
score_dict[good] = 0
for simi_user in user_set:
similarity = simi_matrix[user_dict[user], user_dict[simi_user]]
score_dict[good] += similarity * user_score[simi_user][good]
return score_dict
- 首先调用 get_user_similarity() 函数来获取当前用户的最相似用户集合。
simi_user_list = set(get_user_similarity(simi_matrix, user, user_num, user_dict))
- 接着获取商品的购买用户集合,计算这两个用户集合的交集。
score_dict = {}
for good in records:
user_list = set(items_dict[good])
user_set = simi_user_list & user_list
if good not in score_dict:
score_dict[good] = 0
# ...
- 然后根据用户集合以及商品,获得用户集合中各用户对该商品的评分。最后通过评分公式计算当前用户对该商品的得分。
for simi_user in user_set:
similarity = simi_matrix[user_dict[user], user_dict[simi_user]]
score_dict[good] += similarity * user_score[simi_user][good]
【测试代码】:
>>> cal_score(simi_matrix, items_dict, 'C', 2, user_dict, records, user_score)
{1: 2.6666666666666665, 2: 3.6329931618554525, 3: 0.8164965809277261}
至此,我们完成了基于用户的协同算法。
两者区别
基于用户的协同过滤(User CF)和基于物品的协同过滤(Item CF)在算法上十分类似,推荐系统选择哪种算法,主要取决于推荐系统的考量指标。两者主要的优缺点总结如下。
推荐的场景
Item CF 利用物品间的相似性进行推荐,所以假如用户的数量远远超过物品的数量,可以考虑使用 Item CF,例如购物网站,因为物品的数据相对稳定,因此计算物品的相似度时不但计算量较小,而且不必频繁更新;
UserCF 更适合做新闻、博客或者微内容的推荐系统,因为其内容更新频率非常高,特别是在社交网络中,UserCF 是一个更好的选择,可以增加用户对推荐解释的信服程度。但是在非社交网络中,例如给某个用户推荐一本书,系统给出的解释是某某和你有相似兴趣的人也看了这本书,这很难让用户信服,因为用户可能根本不认识那个人;但假如给出的理由是因为这本书和你以前看过的某本书相似,这样的解释相对合理,用户可能就会采纳系统的推荐。
【UserCF 场景】:一般用在新闻类网站中,更注重社会化。
- 指标:因为在新闻类网站中,用户的兴趣爱好往往比较粗粒度,很少会有用户说只看某个话题的新闻,而且往往某个话题也不是每天都会有新闻。个性化新闻推荐更强调新闻热点,热门程度和时效性是个性化新闻推荐的重点,个性化是补充,所以 UserCF 给用户推荐和他有相同兴趣爱好的人关注的新闻,这样在保证热点和时效性的同时,兼顾个性化。
- 技术:新闻作为一种物品更新非常快,随时会有新的新闻出现,如果使用 ItemCF 的话,需要维护一张物品之间相似度的表,实际工业界这张表一般是一天一更新,这在新闻领域是万万不能接受的。
【ItemCF 场景】:在图书、电子商务和电影网站等领域,ItemCF 能更好地发挥作用,因为在这些网站中,用户的兴趣爱好一般是比较固定的,而且相比于新闻网站更加细腻。在这些网站中,个性化推荐一般是给用户推荐他自己领域的相关物品。另外,这些网站的物品数量更新速度不快,相似度表一天一次更新可以接受。而且,在这些网站中,用户数量往往远大于物品数量,从存储的角度来讲,UserCF 需要消耗更大的空间复杂度,另外 ItemCF 可以方便地提供推荐理由,增加用户对推荐系统的信任度,所以更适合这些网站。
系统多样性
系统多样性,也称为覆盖率,指一个推荐系统能否给用户提供多种选择。
- ItemCF:多样性要远远好于 UserCF,因为 ItemCF 只推荐指定类型的物品给用户,这样它有限的推荐列表中就可能包含了一定数量的非热门的长尾物品。虽然 ItemCF 的推荐对单个用户而言,显然多样性不足,但是对整个系统而言,因为不同用户的主要兴趣点不同,所以系统的覆盖率会比较好。
- UserCF:倾向于推荐热门的物品,因此推荐长尾物品的能力不足。
ItemCF 的推荐有很好的新颖性,容易发现并推荐长尾里的物品。所以大多数情况,ItemCF 的推荐稍微小于 UserCF,但是如果考虑多样性,ItemCF 要比 UserCF 要好很多。
用户特点对推荐算法影响的比较
对于 User CF,推荐的原则是假设用户会喜欢那些和他有相同喜好的用户喜欢的东西,但是假如用户暂时找不到兴趣相投的邻居,那么 User CF 的推荐效果就会大打折扣,因此用户是否适应 User CF 算法跟他有多少邻居成正比关系。
对于 Item CF,推荐的原则是假设用户会喜欢那些和他以前买过的物品相同类型的物品,那么我们可以计算一个用户喜欢的物品的自相似度。一个用户所喜欢的物品的自相似度大,则说明他喜欢的东西都是比较相似的(例如某个用户在淘宝上只买家具),即该用户比较符合 Item CF 方法的基本假设,那么他对 Item CF 的适应度自然比较好。