奇异值分解在协同过滤推荐算法中的应用
目录
推荐算法介绍
协同过滤算法
电影推荐系统
基于用户(项目)的协同过滤算法
基于模型的协同过滤算法
对缺失的地方进行填充---传统SVD
Funk-SVD
推荐算法介绍
所谓推荐算法,即根据用户的历史信息,去寻找每个用户可能关心的项目。例如淘宝根据用户的历史购买记录,用推荐算法去寻找每个用户关心的产品。抖音根据用户的浏览记录,用推荐算法去寻找每个用户关心的视频等等。
在推荐算法中比较常用的是协同过滤算法。本文主要详细介绍协同过滤是算法。
协同过滤算法
协同过滤算法,一般分为三大类:
1、基于用户的协同过滤算法。
2、基于项目的协同过滤算法。
3、基于模型的协同过滤算法。
在下文中用一个简单的例子,对每类进行具体介绍和算法实现。
电影推荐系统
已知某电影平台的用户观看电影后给出的电影评分数据如下所示:
__ | 电影1 | 电影2 | 电影3 |
---|---|---|---|
用户1 | 1 | 0 | 7 |
用户2 | 9 | 6 | 4 |
用户3 | 5 | 7 | 0 |
用户4 | 0 | 7 | 1 |
为清晰的讲清楚算法的来龙去脉和流程,数据集不采用繁琐的真实数据进而省去数据预处理,把数据集简化为4x3矩阵。
上表的行名是用户名称,列名是电影名称,表里面的数字为用户对电影的评级,评级为1到10级,评级也大说明用户越喜爱这个电影,0代表用户尚未观看电影。
现在的问题就是把用户未观看的电影,且用户可能关心的电影推荐给用户。怎么去衡量用户对未观看电影的评价,就是本推荐算法解决的问题。
对于以上问题。可以分别从两个角度解决问题,第一是用户的角度,既以用户协同过滤寻求推荐算法,第二是项目的角度,既以项目协同过滤寻求推荐算法。
基于用户(项目)的协同过滤算法。
基于用户的协同过滤算法为:以用户为样本点,寻求用户的相似性,根据用户的相似性推荐对应的电影给对应的用户。
基于项目的协同过滤算法为:以项目为样本点,寻求项目的相似性,根据项目的相似性推荐对应的电影给对应的用户。
在刻画两个样本点相似性的方法时,一般有以下三种:
欧氏距离相似度= 1/(1+两个样本的欧式距离);
余弦相似度=0.5 + 0.5( float(inA.TinB) / la.norm(inA)*la.norm(inB));
皮尔逊相关系数相似度= 0.5 + 0.5 * corrcoef(inA, inB, rowvar = 0)[0][1]
需要注意的是,在计算相似性时,不论从哪个角度,都不要计算没有打分的哪一项。
对于采用哪一种协同过滤的算法,依据是计算相似度时的计算量,本例中,若使用用户协同过滤算法,那么有四个样本点,若使用项目协同算法,有三个样本点,所以使用项目协同算法,明显计算相似度时计算量会优于用户协同算法。
所以这里使用项目协同算法。(在实际情况中,用户必然多余电影的数量。也会使用项目协同算法)
算法流程
(1)计算各个项目之间的相似度,本例中计算D(电影1,电影2),D(电影1,电影3),D(电影2,电影3)。
(2)计算没有评级位置(数据集中为0的点)的评级。
具体代码如下:
import numpy as np
def loadExData():
# 利用SVD提高推荐效果
"""
行:用户
列:电影
值:用户对电影的评级,0表示未评分
"""
return np.mat([[1, 0, 7],
[9, 6, 4],
[5, 7, 0],
[0, 7, 1]])
def ecludSim(inA, inB):
return 1.0 / (1.0 + la.norm(inA - inB)) # 范数的计算方法linalg.norm(),这里的1/(1+距离)表示将相似度的范围放在0与1之间
def pearsSim(inA, inB):
if len(inA) < 3: return 1.0
return 0.5 + 0.5 * corrcoef(inA, inB, rowvar=0)[0][
1] # 皮尔逊相关系数的计算方法corrcoef(),参数rowvar=0表示对列求相似度,这里的0.5+0.5*corrcoef()是为了将范围归一化放到0和1之间
def cosSim(inA, inB):
num = np.dot(inA.T , inB)
denom = np.linalg.norm(inA) * np.linalg.norm(inB)
return 0.5 + 0.5 * (num / denom) # 将相似度归一到0与1之间
# 基于物品相似度的推荐引擎
def standEst(dataMat, user, simMeas, item):
"""standEst(计算某用户未评分电影中,以对该电影和其他物品评分的电影的物品相似度,然后进行综合评分)
Args:
dataMat 训练数据集
user 用户编号
simMeas 相似度计算方法
item 未评分的物品编号
Returns:
ratSimTotal/simTotal 评分(0~10之间的值)
"""
# 得到数据集中的物品数目
n = np.shape(dataMat)[1]
# 初始化两个评分值
simTotal = 0.0
ratSimTotal = 0.0
# 遍历行中的每个物品(对用户评过分的物品进行遍历,并将它与其他物品进行比较)
for j in range(n):
userRating = dataMat[user, j]
# 如果某个物品的评分值为0,则跳过这个物品
if userRating == 0:
continue
# 寻找两个用户都评级的物品
# 变量 overLap 给出的是两个物品当中已经被评分的那个元素的索引ID
# logical_and 计算x1和x2元素的真值。
overLap = np.nonzero(np.logical_and(dataMat[:, item].A > 0, dataMat[:, j].A > 0))[0]
# 如果相似度为0,则两着没有任何重合元素,终止本次循环
if len(overLap) == 0:
similarity = 0
# 如果存在重合的物品,则基于这些重合物重新计算相似度。
else:
similarity = simMeas(dataMat[overLap, item], dataMat[overLap, j])
# print 'the %d and %d similarity is : %f'(iten,j,similarity)
# 相似度会不断累加,每次计算时还考虑相似度和当前用户评分的乘积
# similarity 用户相似度, userRating 用户评分
simTotal += similarity
ratSimTotal += similarity * userRating
if simTotal == 0:
return 0
# 通过除以所有的评分总和,对上述相似度评分的乘积进行归一化,使得最后评分在0~5之间,这些评分用来对预测值进行排序
else:
return ratSimTotal/simTotal
# recommend()函数,就是推荐引擎,它默认调用standEst()函数,产生了最高的N个推荐结果。
# 如果不指定N的大小,则默认值为3。该函数另外的参数还包括相似度计算方法和估计方法
def recommend(dataMat, simMeas=cosSim, estMethod=standEst):
retMat=dataMat.copy()
n=np.shape(dataMat)[0]
for user in range(0,n):
# 寻找未评级的物品
# 对给定的用户建立一个未评分的物品列表
unratedItems = np.nonzero(dataMat[user, :].A == 0)[1]
# 如果不存在未评分物品,那么就退出函数
if len(unratedItems) == 0:
continue
# 在未评分物品上进行循环
for item in unratedItems:
estimatedScore = estMethod(dataMat, user, simMeas, item)
retMat[user,item]=estimatedScore
# 按照估计得分,对该列表进行排序并返回。列表逆排序,第一个值就是最大值
return retMat
test_data=loadExData()
score_data=recommend(test_data)
print(score_data)
[[1 3 7]
[9 6 4]
[5 7 6]
[4 7 1]]
以上就是基于用户的协同过滤算法。这个算法简单实用,效果还可以,但为了更好的提高效果,经常使用下面基于模型的协同过滤算法。
基于模型的协同过滤算法
注:之所以整理这篇文章,就是因为这个地方,网上相关文章介绍的非常乱,很多概念含糊不清。下面就进行详细介绍。
基于模型的协同过滤算法,多种多样,这里主要介绍应用SVD的基于模型的协同过滤。
还是上面的例子。对于用户-电影评分矩阵
因为矩阵中为0的地方是未评分的地方,可以看出A矩阵是稀疏矩阵,所以A矩阵此时并不能进行SVD分解。针对评分矩阵A稀疏的问题,有两种解决方式,
其一、对缺失的地方进行填充,传统SVD。
其二、是Funk-SVD算法。
对缺失的地方进行填充---传统SVD
对于这方法,用什么值去填充又是一个问题,一般都用矩阵中元素的均值进行填充,其理由是对于用户未观看的电影用既不高也不低的均值进行评分。这种方式还是存在两个问题:
一、若矩阵A太稀疏,则这种方式误差很大,A相对稠密时才可以减小误差。
二、现实问题中,用户和电影数量庞大,这一做法的计算量很大。
对于本例A填充完成后为:
此时,使用SVD进行去燥降维,(去燥是提高效果准确性,若去燥维则,使用填充数据产生的噪音会影响准确性,降维是因为实际问题数据维数庞大)。其SVD分解为:
A=np.array([[1, 4, 7],
[9, 6, 4],
[5, 7, 4],
[4, 7, 1]])
U,S,VT =np.linalg.svd(A)
Sigma=np.diag(S)
print(A)
print('U=',U)
print('Sigma=',Sigma)
print('VT=',VT)
[[1 4 7]
[9 6 4]
[5 7 4]
[4 7 1]]
U= [[-0.35949466 0.89113483 0.02362996 0.27583316]
[-0.63196087 -0.33730085 0.66569561 0.2090525 ]
[-0.53609543 0.04067918 -0.23392266 -0.81007844]
[-0.42894141 -0.30075249 -0.70821689 0.47327163]]
Sigma= [[17.62761582 0. 0. ]
[ 0. 5.70380415 0. ]
[ 0. 0. 3.42546037]]
VT= [[-0.59244457 -0.67989919 -0.43214178]
[-0.55124384 -0.04905129 0.83290108]
[ 0.58748588 -0.73166321 0.34573007]]
去燥降维的方法为在矩阵的SVD分解中取前k大的奇异值对应的U,Sigma,VT 矩阵,分别设为.
k的确定方法为,根据给定的去燥比率,若去燥比率设置为不高于5%,则:
S_square=np.square(S)
Sum=0
k=0
for i in range(3):
Sum=Sum+S_square[i]
rate=1-Sum/sum(S_square)
print(rate)
k=i
if(rate<0.05):
break
print(k)
0.12469622694282334
0.033052897858724783
1
所以当只保留前两个最大的奇异值时,去燥比率就位3.3%,小于5%。
由上面得到,在给定去燥条件的前提下,我们确定了k的值,从而确定了评分矩阵A的降维去燥后的低秩最佳逼近矩阵为:
所以:
A_1 =Sigma[0,0]*np.dot(U[:,0].reshape(-1,1),np.mat(VT[0,:]))+Sigma[1,1]*np.dot(U[:,1].reshape(-1,1),np.mat(VT[1,:]))
print(A_1)
print(A)
[[0.95244684 4.05922338 6.9720154 ]
[7.66034775 7.66842183 3.21162691]
[5.47074821 6.41372353 4.27703102]
[5.42522248 5.22500781 1.83873038]]
[[1 4 7]
[9 6 4]
[5 7 4]
[4 7 1]]
可以看出,去燥后的矩阵和原始矩阵非常相似。也正因如此,用均值去填充的评分变化也不是很大,但是已经发生了变化,这种变化,可以理解为,是由真实评分构成的矩阵的特征改变的(这也是为什么使用SVD的原因),所以如果起始的评分矩阵太过稀疏,真实评分产生的影响就会很小,从而结果的准确性很低。
上面我们得到了去燥后的近似矩阵,然后怎么用这个矩阵呢,在用这个近似矩阵之前,就要先讨论下评分矩阵在SVD分解后,得到的U,Sigma,VT分别有什么物理意义。
由SVD的理论知识得到,对于U矩阵的列向量,是矩阵的特征向量,对于矩阵VT的行向量是矩阵的特征向量。所以要搞清楚U和VT矩阵的物理意义,必须要从和两个矩阵触发。为了讨论一般情况,下面先设A为mxn阶评分矩阵。每一行对应一个用户,每一列对应一部电影。
对于任意n维向量x,Ax是一个m维向量,其第一个元素,可以看作是第一个用户对其所有电影评分的一个加权和,第二个元素为第二个用户对其所有电影的评分的一个加权和。当x固定为衡量每个电影本身某一维度的质量(如音乐的质量)的权重时,x对于用户无关,只取决于电影的音乐本身,那么Ax的第一个元素为用户一对音乐本身的偏好度,Ax的第二个元素为用户二对因为本身的偏好度。这个音乐偏好度和电影无关,只取决于用户本身。
设y=Ax,则从上面的分析,x固定为衡量每个电影本身音乐的质量的权重时,y时衡量用户本身的音乐偏好度。此时在分析,的行代表电影,列代表用户。是一个n维向量,其第一个元素是电影一的所有用户评分的一个加权和,其第二个元素是电影二的所有用户评分的一个加权和,因为权重y是各个用户对音乐本身的偏好,所以,表示的向量维电影音乐的质量。所以此时,的存在是因为权重没有归一化处理。
所以有
所以,对于的每个特征向量都可以理解为电影本身某一维度上的质量分,衡量电影本身不同维度上的特征,和用户没有关系,这里用音乐维度只是方便理解,实际情况中,这些维度并没有真正的解释意义。
所以,根据上面的分析,U的列向量是衡量电影本身某一维度的质量和特征,和用户无关,且越排在前面的向量越能表示电影本身的特征,同理VT的行向量是衡量用户本身某一维度的质量和特征,和电影无关,且越排在前面的向量越能表示用户本身的特征。所以去前k个时,舍掉的都是一些噪音特征。
以上就是这个近似矩阵SVD分解的物理意义,有了这个意义,下面分析怎么使用这个近似矩阵的SVD分解。
对于一个新的用户,其用户评分向量维p=[1,4,0],其中0为未评分,则根绝SVD分解的意义,计算向量(实际上计算的是中(去噪低维空间里)与p对应的向量。),其就是新用户p在去噪低维空间中对应的向量。
然后再用相似度,在中寻找与p最像的向量,找到后,在反推出对应的用户,这样就找到了,去过噪音之后的相似用户,从而用相似用户的评分去填充新用户未评分的电影评分。
上面过程的代码实现如下:
p=[1,4,0]
U_k=U[:,0:2]
Sigma_k=Sigma[0:2,0:2]
VT_k=VT[0:2,:]
p_new=np.dot(np.dot(p,VT_k.T),np.mat(Sigma_k).I)
p_new=np.array(p_new)[0]
#这里用余弦相似度
print(cosSim(p_new,U_k[0,:]))
print(cosSim(p_new,U_k[1,:]))
print(cosSim(p_new,U_k[2,:]))
print(cosSim(p_new,U_k[3,:]))
0.38816907259497146
0.9964791723066383
0.88728868904969
0.9999984582439839
所以在低维空间中,U的第一个行向量和p最接近,所以用户1和新用户最像,所以用用户1的评分去填充新用户未评分的评分,得到p=[1,4,7]
同理如果给的不是新用户评分的向量,而是新电影的评分向量,也可以用相同的方法填充未评分的位置。
从上面的分析不难看出,这种传统的基于SVD的算法,要求待分解的评分矩阵是稠密的,若果是稀疏的,结果准确率会下降,但实际情况评分矩阵就是稀疏的,所以,需要下面的方法继续优化。
Funk-SVD
因为基于传统的SVD的协同过滤存在很大的问题,所以引入Funk-SVD算法,下面对其详细介绍。
由上面传统的SVD分析,得到评分矩阵的近似分解为:
因为是k阶对角矩阵,所以有,此时也是对角矩阵。所以A的近似分解可化简为:
其中,P是mxk阶矩阵,仅与电影本身相关,Q是kxn阶矩阵,仅与用户本身相关。
设,则:
所以PQ矩阵第i低行,第j列的元素
所以,问题就转化为一个优化问题,A的每个元素和矩阵PQ的每个元素之差最小化,既
考虑过拟合问题,我们引入正则项。最后优化问题化简如下:
求解这个优化问题使用梯度下降的方法。
梯度下降
梯度下降算法的介绍在这里不展开了,这里直接求梯度:
所以梯度下降的迭代公式是:
其中为学习效率,
代码实现为:
"""
@输入参数:
R:M*N 的评分矩阵
K:隐特征向量维度
max_iter: 最大迭代次数
alpha:步长
lamda:正则化系数
@输出:
分解之后的 P,Q
P:初始化用户特征矩阵 M*K
Q:初始化物品特征矩阵 N*K,Q 的转置是 K*N
"""
# 给定超参数
K = 5
max_iter = 5000000
alpha = 0.0002
lamda = 1
# 核心算法
def LMF_grad_desc(R, K=2, max_iter=1000, alpha=0.0001, lamda=0.002):
# 定义基本维度参数
M = len(R)
N = len(R[0])
# P、Q 的初始值随机生成
P = np.random.rand(M, K)
Q = np.random.rand(N, K)
Q = Q.T
# 开始迭代
for steps in range(max_iter):
# 对所有的用户 u,物品 i 做遍历,然后对对应的特征向量 Pu、Qi 做梯度下降
for u in range(M):
for i in range(N):
# 对于每一个大于 0 的评分,求出预测评分误差 e_ui
if R[u][i] > 0:
e_ui = np.dot(P[u,:], Q[:,i]) - R[u][i]
# 代入公式,按照梯度下降算法更新当前的 Pu、Qi
for k in range(K):
P[u][k] = P[u][k] - alpha * (2 * e_ui * Q[k][i] + 2 * lamda * P[u][k])
Q[k][i] = Q[k][i] - alpha * (2 * e_ui * P[u][k] + 2 * lamda * Q[k][i])
# u,i 遍历完成,所有的特征向量更新完成,可以得到 P、Q,可以计算预测评分矩阵
predR = np.dot(P, Q)
# 计算当前损失函数(所有的预测误差平方后求和)
cost = 0
for u in range(M):
for i in range(N):
# 对于每一个大于 0 的评分,求出预测评分误差后,将所有的预测误差平方后求和
if R[u][i] > 0:
cost += (np.dot(P[u,:], Q[:,i]) - R[u][i]) ** 2
# 加上正则化项
for k in range(K):
cost += lamda * (P[u][k] ** 2 + Q[k][i] ** 2)
if cost < 0.0001:
# 当前损失函数小于给定的值,退出迭代
break
return P, Q.T, cost
P, Q , cost=LMF_grad_desc(A,K,max_iter,alpha,lamda)
print(A)
print(np.dot(P,Q.T))
print(cost)
[[1 4 7]
[9 6 4]
[5 7 4]
[4 7 1]]
[[1.00366321 3.9974722 6.98747162]
[8.98683502 6.00055608 3.99691414]
[4.99780352 6.99263191 3.99742585]
[3.99994045 6.98860325 1.00426985]]
0.7409324229378083
以上就是梯度下降法的推导和实现。