一、SVD概念
利用SVD实现,我们能够用小得多的数据集来表示原始数据集。这样做,实际上是去除了噪声和冗余数据。
当我们试图节省空间时,去除信息就是很有用了。但是在这里我们则是从数据中抽取信息。基于这个视角,我们就可以把SVD看成是从噪声数据中抽取相关特征。
二、SVD能够进行数据约减的原因
1、在很多情况下,数据中的一小段携带了数据集中的大部分信息,其他信息则要么是噪声,要么是毫不相关的信息。
2、SVD是一种常见的矩阵分解技术,SVD将原始的数据集矩阵Data分解成三个矩阵,如下公式所示:
由公式可以看出如果原始矩阵Data是m行n列,那么分解成的三个矩阵U,,V依次是m行m列,m行n列,n行n列。
矩阵的对角元素是从大到小排列的。这些对角元素成为奇异值(Singular Value),他们对应了原始数据集矩阵Data的奇异值。奇异值和特征值是有关系的,这里的奇异值就是矩阵Data*(Data的转置)特征值的平方根。
因为矩阵的对角元素是从大到小排列的,在科学和工程中,一直存在这样一个普遍事实:在某个奇异值的数目(r个)之后,其他的奇异值都置为0。这就意味着数据集中仅有r个重要特征,而其余特征则都是噪声或者冗余特征。
三、基于Python的SVD实现以及将数据映射到低维空间的过程
1、Python实现:
<span style="font-size:18px;">dataMat = [[1,1,1,0,0],
[2,2,2,0,0],
[1,1,1,0,0],
[5,5,5,0,0],
[1,1,0,2,2],
[0,0,0,3,3],
[0,0,0,1,1]]
>>> dataMat = mat(dataMat)
>>> U,Simga,VT = linalg.svd(dataMat)
>>> U
matrix([[ -1.77939726e-01, -1.64228493e-02, 1.80501685e-02,
9.53086885e-01, -3.38915095e-02, 2.14510824e-01,
1.10470800e-01],
[ -3.55879451e-01, -3.28456986e-02, 3.61003369e-02,
-5.61842993e-02, -6.73073067e-01, -4.12278297e-01,
4.94783103e-01],
[ -1.77939726e-01, -1.64228493e-02, 1.80501685e-02,
-2.74354465e-01, -5.05587078e-02, 8.25142037e-01,
4.57226420e-01],
[ -8.89698628e-01, -8.21142464e-02, 9.02508423e-02,
-1.13272764e-01, 2.86119270e-01, -4.30192532e-02,
-3.11452685e-01],
[ -1.33954753e-01, 5.33527340e-01, -8.35107599e-01,
6.10622664e-16, 1.11022302e-16, 8.88178420e-16,
1.11022302e-16],
[ -2.15749771e-02, 7.97677135e-01, 5.13074760e-01,
-6.06319451e-03, -2.14803071e-01, 1.00648733e-01,
-2.09028015e-01],
[ -7.19165903e-03, 2.65892378e-01, 1.71024920e-01,
1.81895835e-02, 6.44409213e-01, -3.01946200e-01,
6.27084044e-01]])
>>> Simga
array([ 9.72140007e+00, 5.29397912e+00, 6.84226362e-01,
1.52344501e-15, 2.17780259e-16])
>>> VT
matrix([[ -5.81200877e-01, -5.81200877e-01, -5.67421508e-01,
-3.49564973e-02, -3.49564973e-02],
[ 4.61260083e-03, 4.61260083e-03, -9.61674228e-02,
7.03814349e-01, 7.03814349e-01],
[ -4.02721076e-01, -4.02721076e-01, 8.17792552e-01,
5.85098794e-02, 5.85098794e-02],
[ -7.06575299e-01, 7.06575299e-01, -2.22044605e-16,
2.74107087e-02, -2.74107087e-02],
[ 2.74107087e-02, -2.74107087e-02, 2.18575158e-16,
7.06575299e-01, -7.06575299e-01]])</span>
上面可以看到:Simga只有一行,这是因为,由于矩阵除了对角元素其他均为0,因此这种仅返回对角元素的方式能够节省空间,这就是由Numpy的内部机制产生的。我们所要记住的就是:一旦看到Sigma就知道他是一个矩阵。
上面可以看到:Simga中前三个数值比其他的值大多了,所以我们的原始数据集Data可以用如下结果近似:
下面看我们的近似结果(接着上面的代码):
>>> Sig3 = mat([[Simga[0],0,0],[0,Simga[1],0],[0,0,Simga[2]]])
>>> Sig3
matrix([[ 9.72140007, 0. , 0. ],
[ 0. , 5.29397912, 0. ],
[ 0. , 0. , 0.68422636]])
>>> U[:,:3]*Sig3*VT[:3,:]
matrix([[ 1.00000000e+00, 1.00000000e+00, 1.00000000e+00,
-1.51788304e-17, -1.02999206e-17],
[ 2.00000000e+00, 2.00000000e+00, 2.00000000e+00,
1.73472348e-18, 1.12757026e-17],
[ 1.00000000e+00, 1.00000000e+00, 1.00000000e+00,
7.61977287e-16, 7.66747776e-16],
[ 5.00000000e+00, 5.00000000e+00, 5.00000000e+00,
6.59194921e-17, 9.02056208e-17],
[ 1.00000000e+00, 1.00000000e+00, -7.21644966e-16,
2.00000000e+00, 2.00000000e+00],
[ 1.66533454e-16, 1.30451205e-15, -8.88178420e-16,
3.00000000e+00, 3.00000000e+00],
[ 6.24500451e-17, 4.57966998e-16, -3.33066907e-16,
1.00000000e+00, 1.00000000e+00]])
>>>
可以看到和原始数据没什么差别。
问题:我们是如何知道保留前三个奇异值的呢?
策略1:确保要保留的奇异值的数目有很多启发式的策略,其中一个典型的做法就是保留矩阵中90%的能量信息。为了计算总能量信息,我们将所有的奇异值求其平方和。于是可以将奇异值的平方和累加到总值的90%为止。
策略2:当数据有上万的奇异值时,那么就保留前面的2000或3000个。
四:基于协同过滤的推荐引擎
1、协同过滤(collaborative filtering):是通过用户和其他用户的数据进行对比来实现推荐的。通俗点说就是:
我们不利用用于描述物品的属性来计算物品之间的相似度,而是利用用户对他们的意见来计算相似度。
2、相似度的计算(这里介绍了三种):
种1:欧氏距离
我们希望相似度的值在0--1之间变化,并且物品越相似,他们的相似度值也就越大,我们可以用相似度=1/(1+距离)来表示,当距离为0时,相似度为1.0,当距离非常大时,相似度也就趋近于0
种2:皮尔逊相关系数(pearson correlation)
它度量的是两个向量之间的相似度。它相对于欧氏距离的一个优势是:它对用户评级的量级并不敏感。比如:一个狂躁者对所有物品的评分都是5分,而另一个忧郁着对所有物品的评分都是1分,它会认为这两个向量是相等的。在Numpy中,它的计算是由函数corrcoef进行的。可以看代码实现
种3:余弦相似度(cosine similarity)
计算的是两个向量夹角的余弦值.如果夹角为90度,则相似度为0;如果两个向量的方向相同,则相似度为1.0。我们采用两个向量的预选相似度的定义如下:
下面代码实现:
from numpy import *
from numpy import linalg as la
#欧氏距离
def ecludSim(inA, inB):
return 1.0/(1.0 + la.norm(inA - inB))
#皮尔逊相关系数
def pearsSim(inA, inB):
if len(inA) < 3 : return 1.0
return 0.5+0.5*corrcoef(inA,inB,rowvar=0)[0][1]
#余弦相似度
def cosSim(inA, inB):
num = float(inA.T*inB)
denom = la.norm(inA)*la.norm(inB)
return 0.5+0.5*(num/denom)
测试代码:
>>> dataMat
matrix([[1, 1, 1, 0, 0],
[2, 2, 2, 0, 0],
[1, 1, 1, 0, 0],
[5, 5, 5, 0, 0],
[1, 1, 0, 2, 2],
[0, 0, 0, 3, 3],
[0, 0, 0, 1, 1]])
>>> import svdRec
>>> svdRec.ecludSim(dataMat[:,0],dataMat[:,4])
0.13367660240019172
>>> svdRec.ecludSim(dataMat[:,0],dataMat[:,1])
1.0
其他两个测试,同理。
3、基于物品的相似度还是基于用户的相似度?
一般是倾向于使用基于物品的相似度的计算方法,因为用户的数目通常很多,这个看实际情况而定。
4、推荐引擎的评价
我们采用交叉测试的方法,具体做法是:我们将某些已知的评分值去掉,然后对他们进行测试,然后计算出预测值和真实值之间的差异。
四、在推荐系统中的应用
1、首先我们构建一个基本的推荐引擎,它能够寻找用户没有尝过的菜肴。然后,通过SVD来减少特征空间并提高推荐的效果。
思路:给定一个用户,系统会为此用户返回N个最好的推荐菜。为了实现这一点,我们需要做:
(1)寻找用户没有评级的菜肴,即在用户--物品矩阵中的0值
(2)在用户没有评级的所有物品中,对每个物品预计一个可能的评级分数。
(3)对这些物品从高到底进行排序,返回前N个物品。
代码实现:
#对每一个未评分菜肴预测得分
def standEst(dataMat, user, simMeas, item):
n = shape(dataMat)[1]
simTotal = 0.0;ratSimTotal = 0.0
for j in range(n):
userRating = dataMat[user,j]
if userRating == 0: continue
overlap = nonzero(logical_and(dataMat[:,item].A>0,dataMat[:,j].A>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" % (item, j, similarity))
simTotal += similarity
ratSimTotal += similarity*userRating#归一化,是所有评分值在0--5之间
if simTotal == 0:
return 0
else:
return ratSimTotal/simTotal
#给user推荐的菜肴函数
def recommend(dataMat, user, N=3,simMeas=cosSim,estMethod=standEst):
unratedItems = nonzero(dataMat[user,:].A==0)[1]#寻找未评级的物品
if len(unratedItems) == 0:
return 'you rated everything'
itemScores = []
for item in unratedItems:
estimatedScore = estMethod(dataMat, user, simMeas, item)
print("item:",item,"estimatedScore:",estimatedScore)
itemScores.append((item, estimatedScore))
return sorted(itemScores,key=lambda jj:jj[1],reverse=True)[:N]
下面对用户2没有评分的菜肴进行评分,并将评分高的推荐
>>> myMat = mat([[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]])
>>> svdRec.recommend(myMat,2)
('item:', 1, 'estimatedScore:', 2.0243290220056256)
('item:', 2, 'estimatedScore:', 2.5)
[(2, 2.5), (1, 2.0243290220056256)]<span style="color:#ff6666;">#余弦相似度:对物品2的预测评分值为2.5,对物品1的预测评分值为2.0243</span>
>>> svdRec.recommend(myMat,2,simMeas=svdRec.ecludSim)
('item:', 1, 'estimatedScore:', 2.8266504712098603)
('item:', 2, 'estimatedScore:', 3.0)
[(2, 3.0), (1, 2.8266504712098603)]<span style="color: rgb(255, 102, 102); font-family: Arial, Helvetica, sans-serif;">#欧氏距离:对物品2的预测评分值为3.0,对物品1的预测评分值为2.8266</span>
>>> svdRec.recommend(myMat,2,simMeas=svdRec.pearsSim)
('item:', 1, 'estimatedScore:', 2.0)
('item:', 2, 'estimatedScore:', 2.5)
[(2, 2.5), (1, 2.0)]<span style="color: rgb(255, 102, 102); font-family: Arial, Helvetica, sans-serif;">#皮尔逊相关系数:对物品2的预测评分值为:2.5,对物品1的预测评分值为2.0</span>
>>>
2、利用SVD提高推荐效果
为了更加接近实际情况,假设我们现在又有一个新的数据集,我们首先要知道保留几个奇异值,根据我们前面说的,保留前90%的。计算代码如下:
dataSet = [[2,0,0,4,4,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,5],
[0,0,0,0,0,0,0,1,0,4,0],
[3,3,4,0,3,0,0,2,2,0,0],
[5,5,5,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,5,0,0,5,0],
[4,0,4,0,0,0,0,0,0,0,5],
[0,0,0,0,0,4,0,0,0,0,4],
[0,0,0,0,0,0,5,0,0,5,0],
[0,0,0,3,0,0,0,0,4,5,0],
[1,1,2,1,1,2,1,0,4,5,0]]
>>> U, Sigma, VT = la.svd(mat(dataSet))
>>> Sigma
array([ 1.34342819e+01, 1.18190832e+01, 8.20176076e+00,
6.86912480e+00, 5.29063022e+00, 3.91213561e+00,
2.94562509e+00, 2.35486137e+00, 2.08702082e+00,
7.08715931e-01, 1.15779137e-16])
>>> Sig2 = Sigma**2
>>> sum(Sig2)
496.99999999999966
>>> sum(Sig2)*0.9<span style="color:#ff0000;">#前90%是这么多</span>
447.29999999999973
>>> sum(Sig2[:3])
387.43953785565753
>>> sum(Sig2[:4])
434.62441339532046
>>> sum(Sig2[:5])#前3、4列的值不满足90%,前5列的值满足,所以保留前5列的值。
462.61518152879387
下面我们实现的函数,standEst()函数的功能一样,实现对物品的估计评分值,只不过他用到了SVD来降维
def svdEst(dataMat, user, simMeas, item):
n = shape(dataMat)[1]
simTotal = 0.0;ratSimTotal = 0.0
U, Sigma, VT = la.svd(dataMat)
Sig4 = mat(eye(4)*Sigma[:4])
xformedItems = dataMat.T * U[:,:4] * Sig4.I#利用U矩阵将物品转换到低维空间,道理在哪?
for j in range(n):
userRating = dataMat[user,j]
if userRating == 0 or j == item:
continue
similarity = simMeas(xformedItems[item,:].T,xformedItems[j,:].T)
print("the %d and %d similarity is : %f" % (item,j,similarity))
simTotal += similarity
ratSimTotal += similarity*userRating
if simTotal == 0:
return 0
else:
return ratSimTotal/simTotal
至此,所要说的就说完了,经过我自己的一些对比,我觉得用不用SVD的效果不是特别明显,可能是我数据小的问题吧,而且用不用SVD的结果会有一些不同,但是我不知道哪个好一些,也没有进行测试。有兴趣的可以测试下,按照我们推荐引擎评价中所说的办法。