在之前我也看了很多人写的推荐系统的博客,理论的、算法的都有,多是个人的理解和感悟,虽然很深刻,但是对于自己而言还是不成系统,于是我参考大牛项亮编著的《推荐系统实践》将该领域知识系统整理一遍,与大家一起学习。
本系列对应的代码请查看https://github.com/wangyuyunmu/Recommended-system-practice
上一篇总结了基于用户行为数据的推荐方法——隐语义,本篇继续总结基于用户行为数据的相关推荐方法——图模型。
1,图模型
user-item关系可以用图来表示,下面是user与item的二分图:
寻找user与item之间关系的方法,就成了度量图中两个顶点的相关性。
度量图中两个顶点之间相关性的方法很多,但一般来说图中顶点的相关性主要取决于下面3个要素:
1)两个顶点之间的路径数;
2)两个顶点之间路径的长度;
3)两个顶点之间的路径经过的顶点。
相关性高的点一般具有如下性质:
1)两个顶点之间有很多路径相连;
2)连接两个顶点之间的路径长度都比较短;
3)连接两个顶点之间的路径不会经过出度比较大的顶点。
(出度:从一个点链接其他点的边的数量)
2,基于随机游走的Personal Rank算法
下式是PersonalRank的评价指标,PR可以看作是节点的权值或者重要性。out表示出度。
P
R
(
i
)
=
(
1
−
d
)
r
i
+
d
∑
j
∈
(
i
)
P
R
(
j
)
∣
o
u
t
(
j
)
∣
PR(i)=(1-d)r_i+d\sum_{j\in(i)} \frac{PR(j)}{\vert out(j) \vert}
PR(i)=(1−d)ri+dj∈(i)∑∣out(j)∣PR(j)
r
i
=
{
1
i
=
u
0
i
≠
u
r_i= \begin{cases} 1&{i=u}\\ 0&{i\neq u} \end{cases}
ri={10i=ui=u
理论上初始化图模型节点后,按照以上公式进行随机游走,最后所有节点的权值将会收敛,通过排序就可以得到topN个相关节点。
上面描述的过于抽象,还是举个例子吧。
原谅我没有找到更清晰的图,自己又懒得重新做~~~
上面的二部图表示:user A对item a和c感兴趣,B对a b c d都感兴趣,C对c和d感兴趣。
本文假设每条边代表的感兴趣程度是一样的。现在我们要为user A推荐item,实际上就是计算A对所有item的感兴趣程度。在personal rank算法中不区分user节点和item节点,这样一来问题就转化成:对节点A来说,节点A B C a b c d的重要度各是多少。重要度用PR来表示。
初始赋予 PR(A)=1, PR(B)=PR( C)=PR(a)=PR(b)=PR( c)=PR(d)=0即对于A来说,他自身的重要度为满分,其他节点的重要度均为0。然后开始在图上游走。即,每次都是从PR不为0的节点开始游走,往前走一步。继续游走的概率是α,停留在当前节点的概率是1−α。
第一次游走,从A节点开始,以α概率向下游走,以50%的概率走向a和c,a和c得到了A的部分重要度,PR(a)=PR( c)= α*(1/2)*PR(A),PR(A) = 1-α
第二次游走,分别从节点A a c开始,往前走一步,这样
节点a分得A 1/2∗α的重要度,
节点c分得A 1/2∗α的重要度,
节点A分得a 1/2∗α的重要度,
节点A分得c 1/3∗α的重要度,
节点B分得a 1/2∗α的重要度,
节点B分得c 1/3∗α的重要度,
节点C分得c 1/3∗α的重要度。
最后PR(A)要加上1-α。
def PersonalRank(G, alpha, root, max_depth):
rank = dict()
rank = {x: 0 for x in G.keys()}
rank[root] = 1
for k in range(max_depth):
tmp = {x: 0 for x in G.keys()}
# 取出节点i和他的出边尾节点集合ri
for i, ri in G.items():
# 取节点i的出边的尾节点j以及边E(i,j)的权重wij,边的权重都为1,归一化后就是1/len(ri)
for j, wij in ri.items():
# 这里可以看出前一个step(k)生成的图每个节点以alpha概率向其他相关节点传递PR值,
# 生成新的图,但是每个节点都有1-alpha概率保留PR,所以新图整体少了1-alpha
tmp[j] += alpha * rank[i] / (1.0 * len(ri))
tmp[root] += (1 - alpha)
rank = tmp
lst = sorted(rank.items(), key=lambda x: x[1], reverse=True)
for ele in lst:
print("%s:%.3f, \t" % (ele[0], ele[1]))
return rank
为什么要在原节点加上1-alpha?
(或许我对与图模型还不是特别了解,这里仅自己做一些推测。如果后续有进展将会进一步更新)
以上公式后半部分很好理解,根据从某个节点开始游走的概率alpha,出度out,及节点PR值,得到游走到相应节点的PR值。
生成新的PR值对应的图时,实际上每个节点PR值的生成,都是原图每个节点以游走概率alpha权值以后的结果,所以从整体上来说,新图对应的PR值整体减少了1-alpha,这回造成什么后果呢?如果迭代次数不断加深,PR值将会趋近于0,将1-alpha给了参考用户u,这样整体上各节点PR加和为1,计算出的就是所有顶点相对于u的相关度。
下图是page rank的公式,是另一种迭代方式,1-a平均分配到每个节点。
P
R
(
i
)
=
1
−
d
N
+
d
∑
j
∈
(
i
)
P
R
(
j
)
∣
o
u
t
(
j
)
∣
PR(i)=\frac{1-d}{N}+d\sum_{j\in(i)} \frac{PR(j)}{\vert out(j) \vert}
PR(i)=N1−d+dj∈(i)∑∣out(j)∣PR(j)
3,改进
因为给每个用户推荐的时候都需要在用户二分图上进行随机游走计算和迭代,直到每个PR值收敛,时间复杂度高。所以要对personal算法进行优化,一种优化算法是迭代到某一阈值就停止,一般对结果影响不大,另一种优化通过矩阵求解。
M
i
j
=
{
1
∣
o
u
t
(
i
)
∣
j
∈
o
u
t
(
i
)
0
e
l
s
e
M_{ij}= \begin{cases} \frac{1}{\vert out(i) \vert}& { j \in out(i)} \\ 0& {else} \end{cases}
Mij={∣out(i)∣10j∈out(i)else
r
=
(
1
−
α
)
r
0
+
α
M
T
r
r = (1-\alpha)r_0+\alpha M^Tr
r=(1−α)r0+αMTr
M是一个稀疏矩阵,如下所示:
data, row, col = [], [], []
for u in train:
for v in train[u]:
data.append(1 / len(train[u]))# 保存所有节点出度
row.append(users[u]) # 出度的节点
col.append(items[v]) # 连接的节点
for u in item_user: # user遍历完之后,再次遍历item
for v in item_user[u]:
data.append(1 / len(item_user[u]))
row.append(items[u])
col.append(users[v])
# 行程稀疏矩阵,按照列排列row和col分别代表data位置的索引,shape不太赞同,我觉得应该是len(users)+len(items)而不是len(data)
M = csc_matrix((data, (row, col)), shape=(len(users)+len(items),len(users)+len(items)))
通过训练数据获得稀疏矩阵M,测试的时候,初始化user,稀疏矩阵求逆即可:
def GetRecommendation(user):
seen_items = set(train[user])
# 解矩阵方程 r = (1-a)r0 + a(M.T)r
r0 = [[0] for i in range(len(users)+len(items))]
r0[users[user]][0] = 1 #测试那个user就将该user设置为1,表示从此开始随机游走
r0 = np.array(r0)
r = linalg.gmres(eye(len(users) + len(items)) - alpha * M.T, (1 - alpha) * r0) # gmres(A,b),解决稀疏Ax=b的求解问题,
r = r[0][len(users):] # user 之后的节点才是item
idx = np.argsort(-r)[:N] # 取反是为了从大到小排列
recs = [(id2item[ii], r[ii]) for ii in idx] #返回topN的item与PR值的tuple
return recs
以上是二分图模型的求解思路,在基于物品标签的推荐中,会涉及(user、item、tag)的三分图,相应总结后续补充。