事先声明,这篇博客的适用人群是对SVD零基础以及对tensorflow有一点点基础、并且想理解SVD原理的同学,特别是对即将面试准备知识点的小伙伴比较有帮助。
关键字: 矩阵分解,SVD,Funk-SVD,tensorflow
如下是本篇博客的主要内容:
- SVD基本思想
- Funk-SVD
- Bias-Funk-SVD
- 总结
SVD基本思想
SVD的具体思想是针对
m
×
n
m \times n
m×n的矩阵
A
A
A,可以找到矩阵
U
,
S
,
V
U,S,V
U,S,V这三个矩阵,使得
A
=
U
S
V
A = U S V
A=USV
其中
U
U
U为
m
×
m
m \times m
m×m的矩阵,
V
V
V为
n
×
n
n \times n
n×n的矩阵,且满足
U
T
U
=
I
,
V
T
V
=
I
U^TU=I,V^TV=I
UTU=I,VTV=I,也就是说
U
U
U和
V
V
V都是酉矩阵,S为
m
×
n
m \times n
m×n的矩阵,该矩阵除了主对角线之外其他元素都为0,主对角线元素为奇异值从大到小的排列,具体示意图如下所示,其中
∑
\sum
∑代表的是上述的
S
S
S,
在这里
U
U
U和
V
V
V代表的是奇异值列表所对应的奇异特征向量所组成的矩阵。奇异向量(
u
i
u_i
ui和
v
i
v_i
vi)和奇异值(
σ
i
\sigma_i
σi)的求取过程用下述三个公式来求得,具体细节不展开讨论,因为本篇博客的核心思想是SVD的使用,不在于数学原理。
(
A
A
T
)
u
i
=
λ
i
u
i
(
A
T
A
)
v
i
=
λ
i
v
i
σ
i
=
λ
i
\begin{aligned} (AA^T)u_i=\lambda_iu_i \\ (A^TA)v_i=\lambda_iv_i \\ \sigma_i = \sqrt \lambda_i \end{aligned}
(AAT)ui=λiui(ATA)vi=λiviσi=λi
上述过程揭示了一个道理:存储
A
A
A与存储
U
,
S
,
V
U,S,V
U,S,V没有差别。根据证明,奇异值越大,奇异值所代表的能量越大。换言之,大奇异值对应的奇异向量更能代表原先的矩阵,因而只保留前
r
(
r
<
<
m
,
r
<
<
n
)
r(r << m, r << n)
r(r<<m,r<<n)大个奇异向量的话,能够用很小的矩阵组
U
′
,
S
′
,
V
′
U',S',V'
U′,S′,V′相乘得到
A
′
A'
A′来几乎无损地代表
A
A
A了,这样就能达到矩阵压缩的目的,示意图如下所示。
当然这里讲SVD的目的是为了阐述其在推荐系统中发挥的作用,因而如下会把更多的笔墨放在SVD在推荐系统的应用上。
在推荐系统中,一个常见任务是输入一个用户 p p p,其行为向量为 A i A_i Ai,找到系统中与其相近的用户 p t a r g e t p_{target} ptarget,其行为向量为 A t a r g e t A_{target} Atarget,并且根据 p t a r g e t p_{target} ptarget的兴趣,向 p p p推荐资源。上述任务一个最简单的做法是将系统中所有用户针对所有物品的行为都存储下来,当输入 A i A_i Ai时,将其与系统中所有用户行为做相似度计算,返回与 p p p最相似的用户 p t a r g e t p_{target} ptarget。
上述方案一个最大的问题在于计算量太大,一个成型的系统中可能有几千万用户,且系统中又有几百万种物品,如果无损地存储每个用户的行为矩阵并且将该矩阵存放到线上,而且针对每个输入用户都要遍历整个大矩阵,上述方法占用内存和耗时大地无法接受,因而一个有效的方法是运用上文所提到的SVD算法来高效地找到 p t a r g e t p_{target} ptarget,具体操作如下所示:
- 将上述公式 A ′ = U ′ S ′ V ′ A'=U' S' V' A′=U′S′V′ 转化为 A ′ ( V ′ ) T ( S ′ ) − 1 = U ′ A' (V')^T (S')^{-1}=U' A′(V′)T(S′)−1=U′, 能这样操作是因为 V ′ V' V′是单位正交向量,这个上文已经提过,这里将 ( V ′ ) T ( S ′ ) − 1 (V')^T (S')^{-1} (V′)T(S′)−1记为 W W W, 可以得到 A ′ W = U ′ A' W=U' A′W=U′
- 将 U ′ U' U′作为系统中的用户行为矩阵,当输入用户的行为矩阵 A i A_i Ai时,与 W W W相乘就会得到变换后的行为矩阵 X i X_i Xi
- 将 X i X_i Xi与 U ′ U' U′中的每一行进行相似度的计算,最终得到系统中与 p p p的最相似用户 p t a r g e t p_{target} ptarget
可以看出,上述算法能够大大降低计算量且储存空间也降低了很多。如下是上述例子的Python Demo,仅供参考。
import numpy as np
A = np.array([[5, 5, 0, 5],
[5, 0, 3, 4],
[3, 4, 0, 3],
[0, 0, 5, 3],
[5, 4, 4, 5],
[5, 4, 5, 5]])
# svd
U, S, V = np.linalg.svd(A)
# choose top index coordinate
index = 2
S_1 = np.eye(index) * S[:index]
U_1 = U[:, :index]
V_1 = V[:index, :] # attention!!!
# operator W
W = np.dot(V_1.T, np.linalg.inv(S_1))
# find the similar user p_target in the system
A_i = np.array([[3, 4, 0, 2]])
X_i = np.dot(A_i, W)
p_target_index = np.argmin(np.sum((X_i - U_1) ** 2, 1))
print(p_target_index)
>>> 2 # A[2] = [3, 4, 0, 3]
Funk-SVD
上述SVD算法有两个缺点:
- 把一个高维矩阵分解为3个矩阵,非常耗时。
- 如果原始矩阵 A A A非常稀疏,对于一个用户 p p p,在系统中找到一个相似用户 p t a r g e t p_{target} ptarget,有可能 p t a r g e t p_{target} ptarget与 p p p的行为大相径庭。
由上述两个缺点引入Funk-SVD,该算法的基本思想是将矩阵
A
A
A分解为两个低秩的用户矩阵
P
m
×
k
P_{m\times k}
Pm×k和物品矩阵
Q
k
×
n
Q_{k\times n}
Qk×n,当我们想知道用户
p
i
p_i
pi对物品
q
j
q_j
qj的评分时,直接用与之对应的
P
i
P_i
Pi与
Q
j
Q_j
Qj相乘得到预测的评分,即如下公式,其中
M
i
,
j
^
\hat{M_{i,j}}
Mi,j^代表的是用户
p
i
p_i
pi对物品
q
j
q_j
qj的预测评分,
M
i
,
j
^
=
∑
l
=
1
k
P
i
,
l
Q
l
,
j
\hat{M_{i,j}}=\sum_{l=1}^kP_{i,l}Q_{l,j}
Mi,j^=l=1∑kPi,lQl,j
能够这样做的原因是我们认为用户对物品的评分主要是由这些隐因子影响的,所以这些隐因子代表了用户和物品一部分共有的特征,在物品身上表现为属性特征,在用户身上表现为偏好特征。根据上述描述,该算法的核心思想是找到一种矩阵分解的方法,使 P m × k × Q k × n P_{m\times k} \times Q_{k\times n} Pm×k×Qk×n最大程度地还原 A A A,而寻找最优解的过程可以通过梯度下降来实现,因而系统的计算量并不大。
Funk-SVD和SVD最大的区别在于Funk-SVD不依赖于奇异值和奇异向量,完全靠自主学习来完成,因而不管原始矩阵稀疏与否,该算法都能够学习到一个不错的结果。
Bias-Funk-SVD
Funk-SVD是通过学习用户特征向量和物品特征向量从而预测用户对某一个物品的评分,即用户与物品直接的交互信息。但有些时候用户对于一个物品的评价是和用户的喜好没有太大关系,即除了个性化成分外,用户和物品的固有属性也会对预测造成一定的影响,例如一个不挑剔的用户会对任何电影都打一个不错的分数,或者一部好电影,不管是多么挑剔的用户在看,都会给被打一个不错的分数,因而这个时候就必须考虑那些独立于用户或者独立于物品的偏置因素了。
上述偏置因素可以由如下公式表示,其中
μ
\mu
μ代表的是所有物品的平均分,
b
p
i
b_{p_i}
bpi代表的是用户
p
i
p_i
pi的偏置项,
b
q
j
b_{q_j}
bqj代表的是物品
q
j
q_j
qj的偏置项,
b
i
,
j
=
μ
+
b
p
i
+
b
q
j
b_{i,j}=\mu+b_{p_i}+b_{q_j}
bi,j=μ+bpi+bqj
这样用户
p
i
p_i
pi对物品
q
j
q_j
qj的预测评分为如下公式:
M
i
,
j
^
=
∑
l
=
1
k
P
i
,
l
Q
l
,
j
+
b
i
,
j
\hat{M_{i,j}}=\sum_{l=1}^kP_{i,l}Q_{l,j}+b_{i,j}
Mi,j^=l=1∑kPi,lQl,j+bi,j
值得注意的是,如果在现实情况下有信息缺失的情况,例如 μ \mu μ没办法计算,这里可以将 μ \mu μ也看做可学习的参数。如下是上述例子的Python Demo,仅供参考。
def inference_svd(user_batch, item_batch, user_num, item_num, dim=5):
""" SVD Tensorflow Implementation
:param user_batch: user index batch
:param item_batch: item index batch
:param user_num: user count in system
:param item_num: item count in system
:param dim: embedding dim(k)
:return infer: computing result
"""
with tf.device("/cpu:0"):
# global average bias(mu)
bias_global = tf.get_variable("bias_global", shape=[])
# b_{p_i}
w_bias_user = tf.get_variable("embd_bias_user", shape=[user_num])
bias_user = tf.nn.embedding_lookup(w_bias_user, user_batch, name="bias_user")
# b_{q_j}
w_bias_item = tf.get_variable("embd_bias_item", shape=[item_num])
bias_item = tf.nn.embedding_lookup(w_bias_item, item_batch, name="bias_item")
# P_i
w_user = tf.get_variable("embd_user", shape=[user_num, dim],
initializer=tf.truncated_normal_initializer(stddev=0.02))
embd_user = tf.nn.embedding_lookup(w_user, user_batch, name="embedding_user")
# Q_j
w_item = tf.get_variable("embd_item", shape=[item_num, dim],
initializer=tf.truncated_normal_initializer(stddev=0.02))
embd_item = tf.nn.embedding_lookup(w_item, item_batch, name="embedding_item")
# M_{i,j} = P_iQ_j + b_{i,j}
infer = tf.reduce_sum(tf.multiply(embd_user, embd_item), 1)
infer = tf.add(infer, bias_global)
infer = tf.add(infer, bias_user)
infer = tf.add(infer, bias_item, name="svd_inference")
return infer
总结
本文讲述了SVD的原理以及SVD的两个变种Funk-SVD和Bias-Funk-SVD,后续会继续完善这篇文章,添加PCA和SVD++的相关技术知识点。