1.SVD用于推荐
本文的SVD推荐不是FunkSVD,是利用我们在上篇文章中分析过的SVD分解进行推荐。怎么说呢?这才是真正的SVD推荐!
应用的思路是在基于物品的协同过滤的基础上,利用SVD将物品稀疏表示转化为低维的特征表示。
2.实战
这部分的代码改自机器学习实战的第14章,保证可读性不保证运行效率。
2.1 基于物品的协同过滤
计算过程:
对每个用户u未评分的物品item:
对每个u已评过分的物品i:
计算item和i的相似度
相似度乘以i的评分得到item的评分
把评分加起来得到item的总评分
最后给用户推荐评分较高的k个物品
首先加载数据就不说了
from numpy import corrcoef, mat, shape, nonzero, logical_and
import numpy.linalg as la
def loadExtData():
# mat A
return [[4,4,0,2,2],
[4,0,0,3,3],
[4,0,0,1,1],
[1,1,1,2,0],
[2,2,2,0,0],
[1,1,1,0,0],
[5,5,5,0,0]]
然后定义几个相似度计算的函数,用来计算物品的相似度:
# similarity calculation
def excludSim(inA,inB):
'''
use l2 norm to calculate similarity
normalize -> (0,1]
'''
dis = 1.0/(1.0+la.norm(inA-inB))
return dis
def pearsSim(inA,inB):
'''
user pearson coefficient to calculate similarity
normalize -> (0,1]
'''
if(len(inA) < 3):return 1.0
dis = 0.5+0.5*corrcoef(inA,inB,rowvar=0)[0][1]
return dis
def cosSim(inA,inB):
'''
cosine similarity
normalize -> (0,1]
'''
tmp = float(inA.T*inB)
dis = 0.5+0.5*tmp/(la.norm(inA)*la.norm(inB))
return dis
根据数据集上的信息,计算用户对物品的评分值:
def standEst(dataMat, user, simMean, item):
'''
calculate user's score for item
simMean:similarity calculation method
'''
if(dataMat[user, item] != 0): return dataMat[user, item]
n = shape(dataMat)[1] # number of items
simTotal = 0.0
ratSimTotal = 0.0
for i in range(n):
userRating = dataMat[user, i]
if(userRating == 0 or i == item): continue
# search for users that ever rate two items
overLap = nonzero(logical_and(dataMat[:,i].A>0, dataMat[:,item].A>0))[0]
if(len(overLap) == 0): similarity = 0
else: similarity = simMean(dataMat[overLap, i], dataMat[overLap, item])
simTotal += similarity
ratSimTotal += similarity * userRating
if(simTotal == 0):return 0
else: return ratSimTotal / simTotal # return user's score for item
这里总的评分ratSimTotal 除以总的 simTotal其实是对评分进行归一化,只不过不是归一化到(0,1)之间,而是归一化到评分上下限(0,5)之间。
这个也可以看成对物品的所有评分进行加权求和,每个评分的权重等于相似度除以总的相似度(反映这个评分的可靠性),权重之和为1。这个过程类似于我们求均值的过程,只不过求均值时每个值的权重都是1。
然后对某个用户返回评分最高的N个结果,默认是3个:
# 参数有评分矩阵,用户id,返回几个结果,相似度计算方法,计算用户对物品评分的方法
def recommend(dataMat, user, N=3, simMean=cosSim, estTestMean=standEst):
'''
recommend n items to user based on the specific dataMat
simMean:similarity calculation method
estTestMean:cal user score of item
'''
unRatedItem = nonzero(dataMat[user,:].A == 0)[1] # .A change matrix to array
if(len(unRatedItem) == 0):print('There is nothing to recommend')
retScores = [] # scores of unRatedItems
for item in unRatedItem:
itemScore=estTestMean(dataMat, user, simMean, item) # predicton of user for item
retScores.append((item, itemScore))
return sorted(retScores, key=lambda j:j[1], reverse=True)[:N] # return the top N high rated items
然后我们试验一下,比如对第2个用户进行推荐:
myData = mat(loadExtData())
ans = recommend(myData, 2)
print(ans)
2.2 使用SVD进行基于物品的协同过滤推荐
加上SVD到底有啥不一样呢?其实就是利用SVD分解对数据进行低秩近似,然后计算相似度时,物品也不再是高维的向量,而是转化为低维的特征进行计算。让我们一步一步来,先看看如何低维近似。首先对数据进行SVD分解:
dataMat = loadExtData()
U,S,V = la.svd(dataMat)
print(S,"\n")
然后我们取前3个特征值,进行还原:
S_3 = mat([[S[0],0,0],[0,S[1],0],[0,0,S[2]]])
restoreData = U[:,:3]*S_3*V[:3,:]
print(restoreData)
结果如下:
发现还是差不多的。然后我们重新加载一个更稀疏的数据,利用SVD进行推荐。和之前的区别就是在计算相似度的时候, 使用了物品的低维特征:
# recommender items based on svd
def loadExtData2():
return [[0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 5],
[0, 0, 0, 3, 0, 4, 0, 0, 0, 0, 3],
[0, 0, 0, 0, 4, 0, 0, 1, 0, 4, 0],
[3, 3, 4, 0, 0, 0, 0, 2, 2, 0, 0],
[5, 4, 5, 0, 0, 0, 0, 5, 5, 0, 0],
[0, 0, 0, 0, 5, 0, 1, 0, 0, 5, 0],
[4, 3, 4, 0, 0, 0, 0, 5, 5, 0, 1],
[0, 0, 0, 4, 0, 4, 0, 0, 0, 0, 4],
[0, 0, 0, 2, 0, 2, 5, 0, 0, 1, 2],
[0, 0, 0, 0, 5, 0, 0, 0, 0, 4, 0],
[1, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0],
[1, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0]]
def svdEst(dataMat, user, simMeas, item, k):
if(dataMat[user, item] != 0): return dataMat[user, item]
n = shape(dataMat)[1] # n,11
simTotal = 0.0;ratSimTotal = 0.0
U, S, V = la.svd(dataMat)
S3 = mat(eye(k) * S[:k]) # create a diagonal matrix to save 3 eigenvalues in S
xformedItems = dataMat.T * U[:, :k] * S3.I # reduce dimensions of items
for j in range(n):
userRating = dataMat[user, j]
if(userRating == 0 or j == item): continue
similarity = simMeas(xformedItems[item, :].T, xformedItems[j, :].T)
simTotal += similarity
ratSimTotal += similarity * userRating
if(simTotal == 0): return 0
else: return ratSimTotal / simTotal
def svdRecommend(dataMat, user, N=3, simMean=cosSim, estTestMean=svdEst, k=3):
'''
recommend n items to user based on the specific dataMat
simMean:similarity calculation method
estTestMean:cal user score of item
k:k controls the number of eigenvalues
'''
unRatedItem = nonzero(dataMat[user,:].A == 0)[1] # .A change matrix to array
if(len(unRatedItem) == 0):print('There is nothing to recommend')
retScores = [] # scores of unRatedItems
for item in unRatedItem:
itemScore=estTestMean(dataMat, user, simMean, item, k=k) # predicton of user for item
retScores.append((item, itemScore))
return sorted(retScores, key=lambda j:j[1], reverse=True)[:N] # return the top N high rated items
然后我们就可以着手推荐了。 这里要先确定我们到底要降到几维。
奇异值的减少特别的快,在很多情况下,前10%甚至1%的奇异值的和就占了全部的奇异值之和的99%以上的比例。也就是说,我们也可以用最大的k个的奇异值和对应的左右奇异向量来近似描述矩阵。
计算前几个奇异值平方和的百分比来确定将数据降到多少维合适,这里将90%确定为阈值。也可以自己设一个,比如95%。
myData = mat(loadExtData2())
U, S, V = la.svd(myData)
S *= S
threshold = sum(S) * 0.9
k = 0
for i in range(S.shape[0]+1):
if(sum(S[:i]) >= threshold):
k = i
break
然后我们对第3个用户进行推荐:
svdItems = svdRecommend(myData, user=3, estTestMean=svdEst, k=k)
print(svdItems)
3.优缺点分析
首先看下优点吧,我们需要多次计算物品间相似度,svd将物品的稀疏表示映射到低维空间后,计算量大大减小。
缺点太明显了,正如[1]中所说:
在大规模的数据上,SVD分解会降低程序的速度。SVD分解可以在程序调入时运行一次。在大型系统中,SVD每天运行一次或者其频率更低,并且还要离线运行。
这种降维的过程,有时候可以消去噪声,有时候即使能去噪也会损失很多信息得不偿失,所以k值需要谨慎选择。
另一个问题是,SVD分解方式比较单一,分解得到的特征表示是否一定是用户/物品的一种比较好的表示呢?从这个角度看,获取的特征的方式不够灵活,有时效果就会。。。如果可以自己训练出分解的特征那就完美了,FunkSVD就可以自己训练出特征,传统推荐算法(二)我们会介绍一下FunkSVD及其变种。
参考文献
[1] Peter Harrington. 机器学习实战[M]. 2013.
广告
所有传统推荐算法的代码在:https://github.com/wyl6/Recommender-Systems-Samples/tree/master/RecSys Traditional/MF/SVD
更多精彩内容请移步公众号:推荐算法工程师
感觉公众号内容不错点个关注呗