奇异值分解(Singular Value Decomposition,SVD)是提取信息的强大工具
14.1 SVD的应用
优点:简化数据,去除噪声,提高算法的结果
缺点:数据的转换可能难以理解
14.1.1 隐性语义索引
最早的SVD应用之一就是信息检索。我们称利用SVD的方法为隐性语义索引(Latent Semantic Indexing,LSI)或者隐性语义分析(Latent Semantic Analysis,LSA).
在LSI中,一个矩阵是由文档和词语组成的。当我们在该矩阵上应用SVD时,就会构建出多个奇异值。这些奇异值代表了文档中的概念或主题。当我们查找一个词时,如果我们从上千篇相似的文档中抽取出概念,那么同义词就会映射为同一概念。
14.1.2 推荐系统
SVD的另一个应用就是推荐系统。简单版本的推荐系统能够计算项或者人之间的相似度。更先进的方法则先利用SVD从数据中构建一个主题空间,然后再在该空间下计算其相似度。考虑下土中给出的矩阵,它是由餐馆的菜和品菜师对这些菜的意见构成的。
对上述矩阵进行SVD处理,会得到两个奇异值。
SVD是矩阵分解的一种类型,而矩阵分解是将数据矩阵分解为多个独立部分的过程。
14.2 矩阵分解
- 矩阵分解可以将原始矩阵表示成新的易于处理的形式,这种新形式是两个或多个矩阵的乘积。可以想象成代数中的因子分解。
- **不同的矩阵分解技术具有不同的性质,其中有些更适合于某个应用,有些则更适合于其他应用。**最常见的矩阵分解技术是SVD。SVD将原始的数据集矩阵Data分解成三个矩阵U、Σ和V.T.
如果原始矩阵Data是m行n列,那么U、Σ和V.T就分别是m行m列、m行n列和n行n列。
Σ,该矩阵只有对角元素,其他元素均为0.另一个惯例就是,Σ的对角元素是从大到小排列的。这些对角元素称为奇异值(Singular Value),它们对应了原始数据集矩阵Data的奇异值。
设A为mn阶矩阵,q=min(m,n),AA的q个非负特征值的算术平方根叫作A的奇异值。
- 回想上一章的PCA,我们得到的是矩阵的特征值,它们告诉我们数据集中的重要特征。Σ中的奇异值也是如此。奇异值和特征值是有关系的。这里的奇异值就是矩阵Data*Data.T特征值的平方根。
在科学和工程中,一直存在这样一个普遍事实:在某个奇异值的数目(r个)之后,其他的奇异值都置为0.这就意味着数据集中仅有r个重要特征,而其余特征则都是噪声或冗余特征。
14.3 利用python实现SVD
from numpy import *
U,sigma,VT = linalg.svd([[1,1],[7,7]])
print(sigma)
#[10. 0.]
"""Numpy中对于非对角元素为0的矩阵仅返回对角元素,这种方式可节省空间"""
通过sigma的值,确定有显著影响的元素.
from numpy import *
def loadExData():
return [
[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],
]
data = loadExData()
U,sigma,VT = linalg.svd(data)
print(sigma)
#重构原始矩阵
Sig3 = mat([[sigma[0],0,0],[0,sigma[1],0],[0,0,sigma[2]]])
dataMat = U[:,:3]*Sig3*VT[:3,:]
发现sigma的前3个值比其他值大了很多.于是,去掉最后两个值.那么,我们的原始数据集就可以用如下结果来近似:
如何知道仅需保留前3个奇异值的呢?
确定要保留的奇异值的数目有很多策略,其中一个典型的做法就是保留矩阵中90%的能量信息.为了计算总能量信息,我们将所有的奇异值求其平法和.于是可以将奇异值的平方和累加到总值的90%为止.
14.4 基于协同过滤的推荐引擎
协同过滤(collaborative filtering)是通过将用户和其他用户的数据进行对比来实现推荐的.
当知道了两个用户或两个物品之间的相似度,我们就可以利用已有的数据来预测未知的用户喜好.
14.4.1 相似度计算
倘若我们面对的是食品销售网站,该如何计算相似度?或许可以根据食品的配料,热量,或其他类似信息进行相似度的计算;
倘若我们面对的是餐具行业,我们可能会用大小,材质等来进行相似度的计算;
那么,有没有统一的度量方法呢?
我们不利用专家所给出的重要属性来描述物品从而计算它们之间的相似度,而是利用用户对它们的意见来计算相似度.这就是协同过滤中所使用的方法.它并不关心物品的描述属性,而是严格地按照许多用户的观点来计算相似度.
- 在该数据中,认为距离近的更为相似.我们希望,相似度值在0到1之间变化,并且物品对越相似,它们的相似度值也就越大.我们可以用"相似度=1/(1+距离)"来计算.距离为0时,相似度为1.
- 第二种计算距离的方法是皮尔逊相关系数(Person correlation)。该方法相对于欧氏距离的一个优势在于,它对用户评级的量级并不敏感.比如某个狂躁者对所有物品的评分都是5分,而另一个忧郁者对所有物品的评分都是1分,皮尔逊相关系数会认为这两个向量是相等的.其取值范围从-1到+1,通过0.5+0.5*np.corrcoef()这个函数计算,并且把其取值范围归一化到0到1之间.
- 另一个常用的距离计算方法就是余弦相似度(cosine similarity),其计算的是两个向量夹角的余弦值.夹角为0,相似度为1.余弦相似度的取值范围也在-1到+1之间,因此也要归一化到0到1之间.
相似度计算——假设传入向量都是列向量:基于物品的相似度计算
#相似度计算——假设传入向量都是列向量:基于物品的相似度计算
def ecludSim(inA,inB):
"""欧几里得距离"""
return 1.0/(1.0+la.norm(inA-inB))
def pearsSim(inA,inB):
"""皮尔逊距离"""
#少于3个点时,两个向量完全相关,即能由一条直线拟合
if len(inA) < 3:
return 1.0
return 0.5 + 0.5*corrcoef(inA,inB,rowvar=0)[0][1]
print(corrcoef(inA,inB,rowvar=0))
def cosSim(inA,inB):
"""余弦距离"""
num = float(inA.T*inB)
denom = la.norm(inA)*la.norm(inB)
return 0.5 + 0.5*(num/denom)
14.4.2 基于物品的相似度还是基于用户的相似度?
用哪种相似度计算方法?取决于用户或物品的数目。基于物品相似度计算的时间会随物品数量的增加而增加,基于用户的相似度计算也是这样。如果用户的数目很多,那么我们可能倾向于使用基于物品相似度的计算方法。
对大部分产品导向的推荐引擎而言,用户的数量往往大于物品的数量。
14.4.3 推荐引擎的评价
使用交叉测试的方法。具体的做法是:我们将某些已知的评分值去掉,然后对它们进行预测,最后计算预测值与真实值之间的差异。
通常用于推荐引擎评价的指标是称为最小均方根误差(Root Mean Squared Error,RMSE)的指标,它首先计算均方误差的平均值然后取其平方根。
14.5 示例:餐馆菜肴推荐引擎
假设一个人在家决定外出吃饭,但是他不知道该到哪去吃饭,该点什么菜。我们这个推荐系统可以帮他做到。
14.5.1 推荐未尝过的菜肴
推荐系统的工作过程是:给定一个用户,系统会为此用户返回N个最好的推荐菜。
1)寻找用户没有评级的菜肴,即在用户-物品矩阵中的0值
2)在用户没有评级的所有物品中,对每个物品预计一个可能的评级分数。这就是说,我们认为用户可能会对物品的打分(这就是相似度计算的初衷)
怎么估计的呢?
计算未评分物品与其他物品的相似度,求和之和得到总的相似度,再进行归一化处理,最后的结果作为估计的评分值
3)对这些物品的评分从高到低进行排序,返回前N个物品
#基于物品相似度的推荐引擎
def standEst(dataMat,user,simMeas,item):
"""用来在给定相似度计算方法的条件下,预测未评分物品
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: #如果评分值为0,跳过
continue
#寻找两个用户都评级的物品,返回两个物品当中已经被评分的那个元素
overLap = nonzero(logical_and(dataMat[:,item].A > 0,dataMat[:,j].A > 0))[0] #logical_and逻辑与
print(logical_and(dataMat[:,item].A > 0,dataMat[:,j].A > 0))
print(overLap)
if len(overLap) == 0:
similarity = 0
#存在重合元素,则基于这些重合物品计算相似度
else:
print(dataMat[overLap,item])
similarity = simMeas(dataMat[overLap,item],dataMat[overLap,j])
print('the %d and %d similarity is:%f' % (item,j,similarity))
simTotal += similarity
print(simTotal)
ratSimTotal += similarity * userRating
print(ratSimTotal)
if simTotal == 0:
return 0
else:
return ratSimTotal/simTotal #归一化,最后使评分落在0-5之间
def recommend(dataMat,user,N=3,simMeas=cosSim,estMethod=standEst):
"""N=3:产生最高的N个推荐结果"""
#寻找未评级的物品
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)
itemScores.append((item,estimatedScore))
return sorted(itemScores,key=lambda jj:jj[1],reverse=True)[:N]
14.5.2 利用SVD提高推荐的效果
实际的数据集会比我们用于展示recommand()函数功能的myMat矩阵稀疏的多。
怎么确定一个矩阵的到底需要多少特征呢?
多少奇异值能达到总能量的90%。
def featNum(Sigma,limit=0.9):
"""多少奇异值能达到总能量的90%,返回特征数量"""
sig = Sigma**2
totalEn = sum(sig)
LimEn = totalEn*limit
for i in range(len(Sigma)):
temSum = sum(sig[:i])
if temSum >= LimEn:
return i+1
else:
continue
#基于SVD的评分估计
def svdEst(dataMat,user,simMeas,item):
"""对给定用户给定物品构建一个评分估计值"""
n = shape(dataMat)[1]
simTotal = 0.0
ratSimTotal = 0.0
U,Sigma,VT = la.svd(dataMat)
feat = featNum(Sigma)
Sigfeat = mat(eye(feat)*Sigma[:feat]) #建立对角矩阵
xformedItems = dataMat.T * U[:,:feat]*Sigfeat.I #重构矩阵,将高维数据转到低维空间中
print(dataMat.shape)
print(xformedItems.shape)
print(xformedItems)
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(xformedItems[item,:].T)
print(xformedItems[j,:].T)
print(j)
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
14.5.3 构建推荐引擎面临的挑战
- 考虑到代码的执行效率,SVD分解会降低程序的速度,其可以在程序调用时运行一次。在大型系统中,SVD每天运行一次或者其频率更低,并且还要离线运行。
- 也许我们可以只存储非零元素来节省内存和计算开销?
- 另一个潜在的计算资源浪费来自于相似度得分。在我们的程序中,每次需要一个推荐得分时,都要计算多个物品的相似度得分。
- 冷启动问题:在协同过滤场景下,由于新物品到来时缺乏所有用户对其的喜好信息,因此无法判断每个用户对其的喜好。而无法判断某个用户对其的喜好,也就无法利用该商品。一个解决方案时将推荐问题看作搜索问题,这需要使用到数据的类别标签,这个过程被称为基于内容(content-based)的推荐。
14.6 示例:基于SVD的图像压缩
#图像压缩函数
def printMat(inMat,thresh=0.8):
"""遍历所有矩阵元素,当元素大于阈值时打印1,否则打印0"""
for i in range(32):
for k in range(32):
if float(inMat[i,k]) > thresh:
print(1,end='')
else:
print(0,end='')
print('')
def imgCompress(numSV=3,thresh=0.8):
"""图像压缩。它允许基于任意给定的奇异值数目来重构图像"""
myl = []
#读入图像
for line in open('./0_5.txt').readlines():
newRow = []
for i in range(32):
newRow.append(int(line[i]))
myl.append(newRow)
myMat = mat(myl)
print('****original matrix****')
printMat(myMat,thresh)
U,sigma,VT = la.svd(myMat)
SigRecon = mat(zeros((numSV,numSV)))
for k in range(numSV):
SigRecon[k,k] = sigma[k]
reconMat = U[:,:numSV]*SigRecon*VT[:numSV,:]
print('****reconstructed matrix using %d singular values****' % numSV)
printMat(reconMat,thresh)
14.7 本章小结
SVD是一种强大的降维工具,我们可以利用SVD来逼近矩阵并从中提取重要特征。通过保留矩阵80%-90%的能量,就可以得到重要的特征并去掉噪声。