修改内容 | 修改日期 |
---|---|
修正社交网络关系图:社交图谱的关系图应是无向图,而兴趣图谱是有向图 | 2020-03-27 |
美国著名的第三方调查机构尼尔森调查了影响用户相信某个推荐的因素,调查结果显示,9 成的用户相信朋友对他们的推荐,7 成的用户相信网上其他用户对广告商品的评论。从该调查可以看到,好友的推荐对于增加用户对推荐结果的信任度非常重要。
因此,在社交网络的背景下,推荐系统不单单需要关注用户与物品之间的关系,还要关注用户之间的关系。
在社交网站方面,国外以 Fackbook 和 Twitter 为代表,国内社交网站以新浪微博、QQ 空间等为代表。这些社交网站形成了两类社交网络结构。
【社交网络结构】:
- 社交图谱:好友一般是自己在现实社会中认识的人,比如同事、同学、亲戚等,并且这种好友关系是需要双方确认的,如 Fackbook、QQ 空间。
- 兴趣图谱:好友往往都是现实中互不相识的,只是出于对对方言论的兴趣而建立好友关系,并且这种好友关系也是单向的关注关系,如 Twitter、新浪微博。
需要注意的是,任何一个社会化网站都不是单纯的社交图谱或兴趣图谱。在 QQ 空间中大多数用户联系基于社交图谱,而在微博上大多数用户联系基于兴趣图谱。但在微博中,也会关注现实中的亲朋好友,在 QQ 中也会和部分好友有共同兴趣。
在社交网络中需要表示用户之间的联系,可以用图 G(V, E, W) 定义一个社交网络。其中 V 是顶点集合,每个顶点代表一个用户,E 是边集合,如果用户 V a V_a Va 和 V b V_b Vb 有社交网络关系,那么就有一条边 e ( V a , V b ) e(V_a, V_b) e(Va,Vb) 连接这两个用户, W ( V a , V b ) W(V_a, V_b) W(Va,Vb) 用来定义边的权重。
- 社交图谱:朋友关系是需要双向确认的,因而可以用无向边连接有社交网络关系的用户——无向图;
- 兴趣图谱:朋友关系是单向的,可以用有向边代表这种社交网络上的用户关系——有向图。
【图结构特点】:一般在图中,对于用户顶点 u,我们定义
- out(u):为顶点 u 指向的顶点集合,现实意义是用户 u 关注的用户集合;
- in(u):为指向顶点 u 的顶点集合,现实意义是关注用户 u 的用户集合。
显然,在社交图谱图结构中,out(u) = in(u)。
【社交网络数据】:
- 双向确认的社交网络数据:用户 A 和 B 之间形成好友关系需要通过双方的确认。
- 单向关注的社交网络数据:用户 A 可以关注用户 B 而不需要得到用户 B 的允许。
- 基于社区的社交网络数据:用户之间没有明确的关系,但是这种数据包含了用户属于不同社区的数据。比如知乎中的“话题”,用户可能共同关注了某个话题,他们兴趣相似,但却没有真正建立好友关系。
有了用户之间的网络关系,下一步需要将用户与物品之间的关系融合进来。假设此时我们有 A、B、C、D、E、F 六位用户,且他们之间的关注关系同上图的基于社交图谱的关系图,我们再为每个用户新增购物记录,可列出下表数据。
用户 | 购物物品记录 | 关注用户集合 |
---|---|---|
A | 2 | B |
B | 1, 4 | F |
C | 3 | |
D | 1 | B, C |
E | 1, 4 | B |
F | 3 | B, E |
【主要思路】:有了用户关系数据之后,可以使用用户好友或关注的数据计算用户之间的相似度。
基于共同关注好友比例进行相似度计算
对于用户 u 和用户 v,可以使用他们共同关注好友的比例计算他们的相似度。
【计算公式】:
w
1
(
u
,
v
)
=
∣
o
u
t
(
u
)
⋂
o
u
t
(
v
)
∣
∣
o
u
t
(
u
)
⋅
∣
o
u
t
(
v
)
∣
w_1(u, v) = \frac{|out(u) \bigcap out(v)|}{\sqrt{|out(u) \cdot |out(v)|}}
w1(u,v)=∣out(u)⋅∣out(v)∣∣out(u)⋂out(v)∣
其中 out(u) 表示用户 u 所关注的好友集合,
o
u
t
(
u
)
⋂
o
u
t
(
v
)
out(u) \bigcap out(v)
out(u)⋂out(v) 表示用户 u 和用户 v 所共同关注的好友集合,加上 || 表示集合的数量。
实际上上述公式存在一定的问题,假设用户 u 是一个刚创建的用户,他还没有任何的关注用户集合,也就是说 out(u) = 0,这会导致无论 out(v) 取值是什么,分母 out(u) * out(v) = 0。
那么要如何解决这个问题呢?我们可以为 out(u) 的值加一,这样 out(u) 的最小值就为 1,可以避免零乘的情况出现。这么做会引入其他问题吗?如果用户 u 关注的用户数较小时,加上 1 是否会让结果产生较大的影响?
不用担心这一点,每一位用户都做同样的处理(分母加大),并不会改变他们的相似度排名。我们最后需要的是基于相似度降序的用户列表,并不需要具体的相似度数值。
【修正的计算公式】:
w
1
(
u
,
v
)
=
∣
o
u
t
(
u
)
⋂
o
u
t
(
v
)
∣
(
∣
o
u
t
(
u
)
∣
+
1
)
⋅
(
∣
o
u
t
(
v
)
∣
+
1
)
w_1(u, v) = \frac{|out(u) \bigcap out(v)|}{\sqrt{(|out(u)| + 1) \cdot (|out(v)| + 1)}}
w1(u,v)=(∣out(u)∣+1)⋅(∣out(v)∣+1)∣out(u)⋂out(v)∣
基于共同粉丝比例进行相似度计算
使用共同被关注的用户数量计算用户之间相似度,处理过程同上个相似度计算过程,只不过将 out(u) 修改为 in(u)。
【计算公式】:
w
2
(
u
,
v
)
=
∣
i
n
(
u
)
⋂
i
n
(
v
)
∣
∣
i
n
(
u
)
∣
⋅
∣
i
n
(
v
)
∣
w_2(u, v) = \frac{|in(u) \bigcap in(v)|}{\sqrt{|in(u)| \cdot |in(v)|}}
w2(u,v)=∣in(u)∣⋅∣in(v)∣∣in(u)⋂in(v)∣
同样,这种方式也存在“基于共同关注好友比例进行相似度计算”中的零乘问题——刚创建的用户 u 没有粉丝,因此 in(u) = 0。
【修正的计算公式】:
w
2
(
u
,
v
)
=
∣
i
n
(
u
)
⋂
i
n
(
v
)
∣
(
∣
i
n
(
u
)
∣
+
1
)
⋅
(
∣
i
n
(
v
)
∣
+
1
)
w_2(u, v) = \frac{|in(u) \bigcap in(v)|}{\sqrt{(|in(u)| + 1) \cdot (|in(v)| + 1)}}
w2(u,v)=(∣in(u)∣+1)⋅(∣in(v)∣+1)∣in(u)⋂in(v)∣
【与共同关注好友方法的比较】:
- 共同关注好友:适用于计算粉丝数量不多的用户之间的相似度,例如微博的普通用户;
- 共同被关注好友(粉丝):适用于计算粉丝数量较多的用户之间的相似度,例如微博的大 V。
关注用户集合中关注指定用户的比例
用户 u 关注的用户中有多大比例也关注了用户 v。
【计算公式】:
w
3
(
u
,
v
)
=
∣
o
u
t
(
u
)
⋂
i
n
(
v
)
∣
∣
o
u
t
(
u
)
∣
⋅
∣
i
n
(
v
)
∣
w_3(u, v) = \frac{|out(u) \bigcap in(v)|}{\sqrt{|out(u)| \cdot |in(v)|}}
w3(u,v)=∣out(u)∣⋅∣in(v)∣∣out(u)⋂in(v)∣
代码实现
首先,引入所需的包和数据。
>>> import pandas as pd
>>> import numpy as np
>>> focus = pd.read_csv('data/focus.csv')
>>> focus
userId focus
0 A B
1 B F
2 D B
3 D C
4 E B
5 F B
6 F E
然后,根据用户的关注用户数据,为每一位用户建立关系图。
def build_relation_graph(dataset):
user_list = sorted(list(set(dataset[:, 0]) | set(dataset[:, 1])))
# 初始化关系字典
user_dict = {}
for user in user_list:
user_dict[user] = []
# 循环遍历关注数据
for data in dataset:
user = data[0]
user_dict[user].append(data[1])
# 将列表转换为 frozenset 类型
for user in user_dict:
user_dict[user] = frozenset(user_dict[user])
return user_dict
【说明】:
- 首先,我们先从关注数据集中获取所有的用户信息。考虑到有些用户可能没有关注其他用户但被其他用户的情况,我们需要将数据集的 userId 和 focus 两列先做集合处理,然后取并集,最后将排序的列表存储到 user_list 变量中。
user_list = sorted(list(set(dataset[:, 0]) | set(dataset[:, 1])))
- 接着,初始化关系字典,并循环遍历关注数据集,将同一个用户关注的用户放到该用户对应的列表中
user_dict = {}
for user in user_list:
user_dict[user] = []
for data in dataset:
user = data[0]
user_dict[user].append(data[1])
- 最后,将列表转换为 frozenset 类型,因为在后续操作中需要进行集合操作。
for user in user_dict:
user_dict[user] = frozenset(user_dict[user])
接着,统计每个用户的关注用户数量。
def get_user_focus(dataset, user_list):
user_focus_dict = {}
for user in user_list:
user_focus_dict[user] = 0
for data in dataset:
user = data[0]
if user in user_list:
user_focus_dict[user] += 1
return user_focus_dict
【说明】:get_user_focus() 方法接受两个参数,关注用户数据集 dataset 和用户列表 user_list。根据 user_list 去统计该列表中用户的关注用户数量。
- 首先,先初始化用户关注字典 user_focus_dict。
user_focus_dict = {}
for user in user_list:
user_focus_dict[user] = 0
- 接着,遍历关注用户数据集,统计每个用户的关注用户数量。
for data in dataset:
user = data[0]
if user in user_list:
user_focus_dict[user] += 1
然后,根据计算公式获得用户之间的相似度信息。
def get_similarity_matrix(user_dict, user_focus_dict, user_list):
user_num = len(user_list)
user_similarity = {}
for i in range(user_num):
user_A = user_list[i]
user_similarity[user_A] = {}
for j in range(user_num):
if i == j:
continue
user_B = user_list[j]
user_similarity[user_A][user_B] = len(set(user_dict[user_A]) & set(user_dict[user_B])) / np.sqrt((user_focus_dict[user_A] + 1) * (user_focus_dict[user_B] + 1))
return user_similarity
【说明】:get_similarity_matrix() 方法接受三个参数,关系字典 user_dict、用户关注字典 user_focus_dict 以及用户列表 user_list。
- 首先,统计用户列表的长度,并初始化用户相似度字典。
user_num = len(user_list)
user_similarity = {}
- 接着,在双重循环中,分别获得每个用户与其他用户之间的相似度信息。计算所需的数据都已经在前两个函数中获得。
for i in range(user_num):
user_A = user_list[i]
user_similarity[user_A] = {}
for j in range(user_num):
user_B = user_list[j]
user_similarity[user_A][user_B] = len(set(user_dict[user_A]) & set(user_dict[user_B])) / np.sqrt((user_focus_dict[user_A] + 1) * (user_focus_dict[user_B] + 1))
- 在上述代码中,我们用的计算公式是“基于共同关注好友比例的相似度计算”,我们也可以将其修改为其他两种计算公式。
【代码测试】:
>>> user_dict = build_relation_graph(focus.values)
>>> user_dict
{'A': frozenset({'B'}),
'B': frozenset({'F'}),
'C': frozenset(),
'D': frozenset({'B', 'C'}),
'E': frozenset({'B'}),
'F': frozenset({'B', 'E'})}
>>> user_focus_dict = get_user_focus(focus.values, ['A', 'B', 'C', 'D', 'E', 'F'])
>>> user_focus_dict
{'A': 1, 'B': 1, 'C': 0, 'D': 2, 'E': 1, 'F': 2}
>>> get_similarity_matrix(user_dict, user_focus_dict, ['A', 'B', 'C', 'D', 'E', 'F'])
{'A': {'A': 0.5, 'B': 0.0, 'C': 0.0, 'D': 0.4082482904638631, 'E': 0.5, 'F': 0.4082482904638631},
'B': {'A': 0.0, 'B': 0.5, 'C': 0.0, 'D': 0.0, 'E': 0.0, 'F': 0.0},
'C': {'A': 0.0, 'B': 0.0, 'C': 0.0, 'D': 0.0, 'E': 0.0, 'F': 0.0},
'D': {'A': 0.4082482904638631, 'B': 0.0, 'C': 0.0, 'D': 0.6666666666666666, 'E': 0.4082482904638631, 'F': 0.3333333333333333},
'E': {'A': 0.5, 'B': 0.0, 'C': 0.0, 'D': 0.4082482904638631, 'E': 0.5, 'F': 0.4082482904638631},
'F': {'A': 0.4082482904638631, 'B': 0.0, 'C': 0.0, 'D': 0.3333333333333333, 'E': 0.4082482904638631, 'F': 0.6666666666666666}}
有了基于社交网络的用户相似度数据后,我们就可以根据这相似度数据以及用户的评分数据,去计算物品的评分。
【评分公式】:
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 的得分。
【实现步骤总结】:
- 首先获取用户的关注数据集;
- 根据关注数据集计算基于社交网络的用户相似度数据;
- 再针对用户 u 挑选 k 个最相似的用户,把他们购买过的物品中,u 未购买过的物品推荐给用户 u 即可;
- 如果有评分数据,则可以针对这些物品作进一步的打分。
完整代码,包括三种实现方式以及数据集 传送门
【优缺点】:
- 优点:计算简单,处理容易;
- 缺点:存在用户冷启动问题,且对于大规模的社交关系,离线计算好用户的相似度供线上推荐系统使用,这种做法不合理。
参考
- 《推荐系统与深度学习》