[完]机器学习实战 第十四章 利用SVD简化数据

本章内容:

  • SVD矩阵分解
  • 推荐引擎
  • 利用SVD提升推荐引擎的性能

餐馆可分为很多类别,不同的专家对其分类可能有不同依据。实际中,我们可以忘掉专家,从数据着手,可对记录用户关于餐馆观点的数据进行处理,并从中提取出其背后的因素。这些因素可能会与餐馆的类别、烹饪时采用的某个特定配料,或其他任意对象一致。然后,可利用这些因素来估计人们对没有去过的餐馆的看法。

提取这些信息的方法称为奇异值分解(Singular Value Decomposition,SVD)。从生物信息学到金融学等在内的很多应用中,SVD都是提取信息的强大工具。

本章会介绍SVD的概念及其能进行数据约简的原因,然后,介绍基于Python的SVD实现以及将数据映射到低维空间的过程。还将学习推荐引擎的概念和它们实际运行过程。为提高SVD的精度,我们将会把其应用到推荐系统中,该推荐系统将会帮助人们寻找到合适的餐馆,最后,会讲述一个SVD在图像压缩中的例子。

一、SVD的应用

奇异值分解的优缺点:

  • 优点:简化数据,去除噪声,提高算法的结果。
  • 缺点:数据的转换可能难以理解。
  • 使用数据类型:数值型数据。

利用SVD,可使用小得多的数据集来表示原始数据集,这样会去除噪声数据和冗余信息。在此,我们主要是为了从数据中抽取信息。基于此,可把SVD看成是从有噪声数据中抽取相关特征。

1-1 隐形语义索引

最早的SVD应用之一是信息检索。将利用SVD的方法称为隐性语义索引(Latent Semantic Indexing,LSI)隐性语义分析(Latent Semantic Analysis,LSA)

在LSI中,一个矩阵是由文档和词语组成的。当我们在该矩阵上应用SVD时,就会构建出多个奇异值。这些奇异值代表了文档中的概念或主题,这一特点可以用于更高效的文档搜索。在词语拼写错误时,只基于词语存在与否的简单搜索方法会遇到问题。简单搜索的另一个问题就是同义词的使用。即,当查找一个词时,其同义词所在的文档可能并不会匹配上。如果从上千篇相似的文档中抽取出概念,那么同义词就会映射为统一概念。

1-2 推荐系统

SVD的另一个应用是推荐系统。简单版本的推荐系统能计算项或人之间的相似度。更先进的方法则先利用SVD从数据中构建一个主题空间,然后再在该空间下计算其相似度。图1给出的矩阵,由餐馆的菜和品菜师对这些菜的意见构成。品菜师可采用1-5之间的任意整数来对菜评级。若没品尝过某道菜,则评0级。


餐馆的菜及其评级的数据。对此矩阵进行SVD处理则可以将数据压缩到若干概念中。在右边的矩阵当中,标出了一个概念。
图1 餐馆的菜及其评级的数据。对此矩阵进行SVD处理则可以将数据压缩到若干概念中。在右边的矩阵当中,标出了一个概念。

对上述矩阵进行SVD处理,会得到两个奇异值。因此,就会仿佛有两个概念或主题与此数据集相关联。看看能否通过观察图中的0来找到这个矩阵的具体概念。观察有图的阴影部分,看起来Ed、Peter和Tracy对“烤牛肉”和“手撕猪肉”进行了评级,同时这三人未对其他菜评级。烤牛肉和手撕猪肉都是美式烧烤餐馆才有的菜,其他菜则在日式餐馆才有。

可以将奇异值想象成一个新空间。与图1中的矩阵给出的五维或者七维不同,我们最终的矩阵只有二维。这二维分别是什么呢?能告诉我们数据的什么信息?这二维分别对应图中给出的两个组,右图中已经标示出了其中的一个组。可基于每个组的共同特征来命名这二维,比如得到的美式BBQ和日式食品这二维。

如何将原始数据变换到上述新空间中呢?下一节会进一步详细地介绍SVD,将会了解到SVD是如何得到 U VT 两个矩阵。 VT 矩阵会将用户映射到BBQ/日式食品空间去。类似地, U 矩阵会将餐馆的菜映射到BBQ/日式食品空间去。真实的数据通常不会像图1中的矩阵那样稠密或整齐,这里如此只是为了便于说明问题。

推荐引擎中可能会有噪声数据,比如,某人对某些菜的评级就可能存在噪声,并且推荐系统也可将数据抽取为这些基本主题。基于这些主题,推荐系统就能取得比原始数据更好的推荐效果。

二、矩阵分解

在很多情况下,数据中的一小段携带了数据集中的大部分信息,而其他信息要么是噪声,要么就是毫不相关的信息。矩阵分解可将原始矩阵表示成新的易于处理的形式,新形式是两个或多个矩阵的乘积。

不同的矩阵分解技术具有不同的性质,其中有些更适合于某个应用,有些则更适合于其他应用。最常见的一种矩阵分解技术就是SVD。SVD将原始的数据集矩阵Data分解成三个矩阵 U Σ VT 。如果原始矩阵Data是m行n列,则有如下等式:

Datam×n=Um×mΣm×nVTn×n

上述分解中会构建出一个矩阵 Σ ,该矩阵只有对角元素,其他元素均为0。另一个惯例就是, Σ 的对角元素是从大到小排列的。这些对角元素称为奇异值(Singular Value),它们对应了原始数据集矩阵Data的奇异值。回想PCA章节,得到的是矩阵的特征值,它们告诉我们数据集中的重要特征。 Σ 中的奇异值也是如此。奇异值和特征值时有关系的。这里的奇异值就是矩阵 DataDataT 特征值的平方根。

矩阵 Σ 只有从大到小排列的对角元素。在科学和工程中,一致存在这样一个普遍事实:在某个奇异值的数目(r个)之后,其他的奇异值都置为0。这就意味着数据集中仅有r个重要特征,而其余特征则都是噪声或冗余特征。

三、利用Python实现SVD

NumPy由一个称为linalg的线性代数工具箱,利用此工具箱可实现如下矩阵的SVD处理:

[1717]

要在Python上实现该矩阵的SVD处理,执行如下命令:

>>> from numpy import *
>>> U,Sigma,VT=linalg.svd([[1,1],[7,7]])
>>> U
array([[-0.14142136, -0.98994949],
       [-0.98994949,  0.14142136]])
>>> Sigma
array([ 10.,   0.])
>>> VT
array([[-0.70710678, -0.70710678],
       [-0.70710678,  0.70710678]])

注意,矩阵Sigma以行向量array([ 10., 0.])返回,而非如下矩阵:

array([[ 10.,   0.],
       [  0.,   0.]])

由于矩阵除了对角元素其他均为0,因此这种仅返回对角元素的方式能够节省空间,这是由NumPy的内部机制产生的。接下来在一个更大的数据集上进行更多的分解。建立新文件svdRec.py并加入如下代码:

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]]

接下来对该矩阵进行分解。

>>> import ml.svdRec as svdRec
>>> Data=svdRec.loadExData()
>>> U,Sigma,VT=linalg.svd(Data)
>>> Sigma
array([  9.72140007e+00,   5.29397912e+00,   6.84226362e-01,
         1.67441533e-15,   3.39639411e-16])

前三个数据比其他的值大很多,后两个值在不同机器上结果可能会稍有差异,但数量级差不多。于是,我们可将后两个值去掉。原始数据集可用如下结果来近似:

Datam×nUm×3Σ3×3VT3×n

图2是上述近似计算的一个示意图。


SVD的示意图。矩阵Data被分解。浅灰色区域时原始数据,深灰色区域是矩阵近似计算仅需要的数据
图2 SVD的示意图。矩阵Data被分解。浅灰色区域时原始数据,深灰色区域是矩阵近似计算仅需要的数据

接着可重构原始矩阵,首先构建一个3x3的矩阵Sig3:

>>> Sig3=mat([[Sigma[0], 0, 0],[0, Sigma[1], 0],[0, 0, Sigma[2]]])

接下来重构原始矩阵的近似矩阵。由于Sig3仅为3x3的矩阵,因而只需使用矩阵 U 的前3列和 VT 的前三行。为了在Python中实现这一点,输入如下命令:

>>> U[:,:3]*Sig3*VT[:3,:]
matrix([[  1.00000000e+00,   1.00000000e+00,   1.00000000e+00,
          -6.81963166e-17,  -7.18826040e-17],
        [  2.00000000e+00,   2.00000000e+00,   2.00000000e+00,
           7.52436308e-17,   6.76542156e-17],
        [  1.00000000e+00,   1.00000000e+00,   1.00000000e+00,
           7.91142325e-16,   7.87456038e-16],
        [  5.00000000e+00,   5.00000000e+00,   5.00000000e+00,
           8.50014503e-17,   6.63531730e-17],
        [  1.00000000e+00,   1.00000000e+00,  -8.88178420e-16,
           2.00000000e+00,   2.00000000e+00],
        [  1.66533454e-16,   1.52655666e-15,  -1.44328993e-15,
           3.00000000e+00,   3.00000000e+00],
        [  2.77555756e-17,   4.78783679e-16,  -4.02455846e-16,
           1.00000000e+00,   1.00000000e+00]])

如何知道仅需保留前3个奇异值呢?确定要保留的奇异值的数目有很多启发式的策略,其中一个典型的做法是保留矩阵中90%的能量信息。为计算总能量信息,将所有的奇异值求其平方和。于是可将奇异值的平方和累加到总值的90%为止。另一个启发式策略是,当矩阵上有上万的奇异值时,那么就保留前面的2000或3000个。在任何数据集上,都不能保证前3000个奇异值能够包含90%的能量信息,但在实际中更容易实施。

通过三个矩阵对原始矩阵进行了近似,可用一个小很多的矩阵来表示一个大矩阵。很多应用可通过SVD来提升性能。接下来将讨论一个比较流行的SVD应用的例子——推荐引擎。

四、基于协同过滤的推荐引擎

有很多方法可实现推荐功能,这里使用一种称为协同过滤(collaborative filtering)的方法。协同过滤是通过将用户和其他用户的数据进行对比来实现推荐的。

这里的数据是从概念上组织成了类似图2所给出的矩阵形式。当数据采用这种方式进行组织时,我们就可比较用户或物品之间的相似度了。当知道两个用户或两个物品之间的相似度,就可利用已有的数据来预测未知的用户喜好。如,我们试图对某个用户喜欢的电影进行预测,推荐引擎会发现有一部电影该用户还没看过。然后,它就会计算该电影和用户看过的电影之间的相似度,如果相似度高,推荐算法就会认为用户喜欢这部电影。

在上述场景下,唯一所需要的数学方法就是相似度计算。我们首先讨论物品之间的相似度计算,然后讨论在基于物品和基于用户的相似度计算之间的折中。最后,介绍推荐引擎成功的度量方法。

4-1 相似度计算

希望拥有一些物品之间相似度的定量方法。我们不利用专家所给出重要属性来描述物品从而计算它们之间的相似度,而是利用用户对它们的意见来计算相似度。这就是协同过滤中所使用的的方法。它并不关心物品的描述属性,而是严格地按照许多用户的观点来计算相似度。图3给出了有一些用户及其对前面给出的部分菜肴的评级信息所组成的矩阵。


用于展示相似度计算的简单矩阵
图3 用于展示相似度计算的简单矩阵

计算一下手撕猪肉和烤牛肉之间的相似度。一开始使用欧氏距离来计算。

(44)2+(33)2+(21)2=1

而手撕猪肉和鳗鱼饭的欧式距离为:

(42)2+(35)2+(22)2=2.83

在该数据中,由于手撕猪肉和烤牛肉的距离小于手撕猪肉和鳗鱼饭的距离。因此手撕猪肉与烤牛肉比鳗鱼饭更为相似。我们希望,相似度值在0到1之间变化,并且物品对越相似,它们的相似度值也就越大。可用“相似度=1/(1+距离)”这样的算式来计算相似度。当距离为0时,相似度为1.0。如果距离真的非常大时,相似度也就趋近于0。

第二种计算距离的方法是皮尔逊相关系数(Pearson correlation)。在度量回归方程的精度时曾经用到过这个量,它度量的是两个向量之间的相似度。该方法相对于欧式距离的一个优势在于,它对用户评级的量级并不敏感。比如,某个狂躁者对所有物品的评分都是5分,而另一个忧郁者对所有物品的评分都是1分,皮尔逊相关系数会认为这两个向量时相等的。在NumPy中,皮尔逊相关系数的计算是由函数corrcoef()进行的,后面很快就会用到它了。皮尔逊相关系数取值范围从-1到+1,可通过0.5+0.5*corrcoef()这个函数计算,并且把其取值范围归一化到0到1之间。

另一个常用的距离计算方法是余弦相似度(cosine similarity),其计算的是两个夹角的余弦值。如果夹角为90度,则相似度为0;如果两个向量的方向相同,则相似度为1.0。同皮尔逊相关系数一样,余弦相似度的取值范围也在-1到+1之间,因此也需将它归一化到0到1之间。计算余弦相似度,采用的两个向量 A B 夹角的余弦相似度的定义如下:

cosθ=ABAB

其中,表示向量 A B 的2范数,你还可以定义向量的任一范数,但是如果不指定范数阶数,则都假设为2范数。向量[4, 2, 2]的2范数为:

42+22+22

NumPy的线性代数工具箱中提供了范数的计算方法linalg.norm()。

将上述各种相似度的计算方法写成Python中的函数。

from numpy import *
from numpy import linalg as la
# inA和inB都是列向量
def ecludSim(inA, inB) :
    return 1.0/(1.0 + la.norm(inA - inB))

def pearsSim(inA, inB) :
    # 检查是否存在三个或更多的点,若不存在,则返回1.0,这是因为此时两个向量完全相关
    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)

下面我们尝试执行上述函数:

>>> import ml.svdRec as svdRec
>>> from numpy import *
>>> myMat = mat(svdRec.loadExData())
# 欧氏距离
>>> svdRec.ecludSim(myMat[:,0], myMat[:,4])
0.13367660240019172
>>> svdRec.ecludSim(myMat[:,0], myMat[:,0])
1.0
# 余弦相似度
>>> svdRec.cosSim(myMat[:,0], myMat[:,4])
0.54724555912615336
>>> svdRec.cosSim(myMat[:,0], myMat[:,0])
0.99999999999999989
# 皮尔逊相关系数
>>> svdRec.pearsSim(myMat[:,0], myMat[:,4])
0.23768619407595826
>>> svdRec.pearsSim(myMat[:,0], myMat[:,0])
1.0

上面的相似度计算都是假设数据采用了列向量的方式进行表示。如果利用上述函数来计算两个行向量的相似度就会遇到问题(我们很容易对上述函数进行修改以计算行向量之间的相似度)。这里采用列向量的表示方法,暗示着我们将利用基于物品的相似度计算方法。

4-2 基于物品的相似度还是基于用户的相似度?

计算两个餐馆菜肴之间的距离,这称为基于物品(item-based)的相似度。计算用户距离的方法则称为基于用户(user-based)的相似度。图3中,行与行之间比较的是基于用户的相似度,列与列之间比较的是基于物品的相似度。使用哪种相似度取决于用户或物品的数目。基于物品相似度计算的时间会随着物品数量的增加而增加,基于用户的相似度计算的时间则会随着用户数量的增加而增加。如果用户的数目很多,那么我们可能倾向于使用基于物品相似度的计算方法。

对于大部分产品导向的推荐引擎而言,用户的数量往往大于物品的数量,即购买商品的用户会多于出售的商品种类。

4-3 推荐引擎的评价

我们既没有预测的目标值,也没有用户来调查他们对预测的满意程度。这里我们就可以采用前面多次使用的交叉测试的方法。具体做法是,将某些已知的评分值去掉,然后对它们进行预测,最后计算预测值和真实值之间的差异。

通常用于推荐引擎评价的指标是最小均方根误差(Root Mean Squared Error,RMSE)。它首先计算均方误差的平均值然后取其平方根。若评级在1到5星这个范围内,而我们得到的RMSE为1.0,那么就意味着预测值和用户给出的真实评价相差一个星级。

五、示例:餐馆菜肴推荐引擎

假设用户在家决定外出吃饭,但他不知道该去哪里吃饭,点什么菜。次推荐系统可帮他。

首先构建一个基本的推荐引擎,它能够寻找用户没有尝过的菜肴。然后,通过SVD来减少特征空间并提高推荐的效果。这之后,将程序打包并通过用户可读的人机界面提供给人们使用。最后,介绍在构建推荐系统时面临的一些问题。

5-1 推荐未尝试过的菜肴

推荐系统的工作过程是:给定一个用户,系统会为此用户返回N个最好的推荐菜。为了实现这一点,则需要做到:

  1. 寻找用户没有评级的菜肴,即在用户-物品矩阵中的0值;
  2. 在用户没有评级的所有物品中,对每个物品预计一个可能的评级分数。这就是说,我们认为用户可能对物品的打分(这就是相似度计算的初衷);
  3. 对这些物品的评分从高到底进行排序,返回前N个物品。

基于物品相似度的推荐引擎代码如下:

# 用来计算在给定相似度计算方法的条件下,用户对物品的估计评分值
# 参数:数据矩阵、用户编号、物品编号、相似度计算方法,矩阵采用图1和图2的形式
# 即行对应用户、列对应物品
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]
        # 如果某个物品评分值为0,意味着用户没有对该物品评分,跳过
        if userRating == 0 : continue
        # 寻找两个用户都评级的物品,变量overLap给出的是两个物品当中已经被评分的那个元素
        overLap = nonzero(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' % (item, j, similarity)
        # 随后相似度不断累加
        simTotal += similarity
        ratSimTotal += similarity * userRating
    if simTotal == 0 : return 0
    # 通过除以所有的评分总和,对上述相似度评分的乘积进行归一化。这使得评分值在0-5之间,
    # 而这些评分值则用于对预测值进行排序
    else : return ratSimTotal/simTotal

# 推荐引擎,会调用standEst()函数,产生最高的N个推荐结果。
# simMeas:相似度计算方法
# estMethod:估计方法
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 :
        # 对于每个未评分物品,通过调用standEst()来产生该物品的预测评分。
        estimatedScore = estMethod(dataMat, user, simMeas, item)
        # 该物品的编号和估计得分值会放在一个元素列表itemScores
        itemScores.append((item, estimatedScore))
    # 寻找前N个未评级物品
    return  sorted(itemScores, key=lambda jj : jj[1], reverse=True)[:N] 

代码实际运行效果:

>>> import ml.svdRec as svdRec
>>> from numpy import *
>>> myMat=mat(svdRec.loadExData())
>>> myMat[0,1]=myMat[0,0]=myMat[1,0]=myMat[2,0]=4
>>> myMat[3,3]=2
>>> myMat
matrix([[4, 4, 1, 0, 0],
        [4, 2, 2, 0, 0],
        [4, 1, 1, 0, 0],
        [5, 5, 5, 2, 0],
        [1, 1, 0, 2, 2],
        [0, 0, 0, 3, 3],
        [0, 0, 0, 1, 1]])
>>> svdRec.recommend(myMat,2)
[(4, 2.5), (3, 1.9703483892927431)]
>>> svdRec.recommend(myMat,2,simMeas=svdRec.ecludSim)
[(4, 2.5), (3, 1.9866572968729499)]
>>> svdRec.recommend(myMat,2,simMeas=svdRec.pearsSim)
[(4, 2.5), (3, 2.0)]

5-2 利用SVD提高推荐的效果

实际的数据集会比用于展示recommend()函数功能的myMat矩阵稀疏得多。图4给出了一个更真实的矩阵的例子。


一个更大的用户-菜肴矩阵,其中有很多物品都没有评分,这比一个全填充的矩阵更接近真实情况
图4 一个更大的用户-菜肴矩阵,其中有很多物品都没有评分,这比一个全填充的矩阵更接近真实情况

可以通过loadExData2()将该矩阵加载到程序中。

def loadExData2():
    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]]

接下来计算该矩阵的SVD来了解其到底需要多少维特征。

>>> reload(svdRec)
<module 'ml.svdRec' from 'C:\Python27\ml\svdRec.pyc'>
>>> from numpy import linalg as la
>>> from numpy import *
>>> U,Sigma,VT=la.svd(mat(svdRec.loadExData2()))
>>> Sigma
array([ 15.77075346,  11.40670395,  11.03044558,   4.84639758,
         3.09292055,   2.58097379,   1.00413543,   0.72817072,
         0.43800353,   0.22082113,   0.07367823])

接着看看到底多少个奇异值能达到总能量的90%。

# 对Sigma中的值求平方
>>> Sig2=Sigma**2
# 计算总能量
>>> sum(Sig2)
541.99999999999955
# 计算总能量的90%
>>> sum(Sig2)*0.9
487.79999999999961
# 计算前两个元素所包含的能量
>>> sum(Sig2[:2])
378.8295595113579
# 前两个元素所包含的能量低于总能量的90%,于是计算前三个元素所包含的能量
>>> sum(Sig2[:3])
500.50028912757926

前三个元素所包含的总能量符合要求,可以将一个11维的矩阵转换成一个3维矩阵。下面对转换后的三维空间构造出一个相似度计算函数。利用SVD将所有的菜肴映射到一个低维空间中去。在低维空间下,可以利用前面相同的相似度计算方法来进行推荐。构建一个类似于standEst()的函数svdEst()

# 基于SVD的评分估计
# 在recommend()中,svdEst用户替换对standEst()的调用,该函数对给定用户物品构建一个评分估计值。
# 与standEst()非常相似,不同之处就在于它在第3行对数据集进行了SVD分解。在SVD分解后,只利用包含
# 90%能量值的奇异值,这些奇异值以Numpy数组的形式得以保存。
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])
    # 利用U矩阵将物品转换到低维空间中
    xformedItems = dataMat.T * U[:, :4] * Sig4.I
    # 对于给定的用户,for循环在用户对应行的所有元素上进行遍历,与standEst()函数中的for循环目的一样
    # 不同的是,这里的相似度是在低维空间下进行的。相似度的计算方法也会作为一个参数传递给该函数
    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便于了解相似度计算的进展情况
        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

查看程序的执行效果:

>>> reload(svdRec)
>>> myMat=mat(svdRec.loadExData2())
>>> myMat
matrix([[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]])
>>> svdRec.recommend(myMat, 1, estMethod=svdRec.svdEst)
the 0 and 3 similarity is : 0.490950
the 0 and 5 similarity is : 0.484274
the 0 and 10 similarity is : 0.512755
the 1 and 3 similarity is : 0.491294
the 1 and 5 similarity is : 0.481516
the 1 and 10 similarity is : 0.509709
the 2 and 3 similarity is : 0.491573
the 2 and 5 similarity is : 0.482346
the 2 and 10 similarity is : 0.510584
the 4 and 3 similarity is : 0.450495
the 4 and 5 similarity is : 0.506795
the 4 and 10 similarity is : 0.512896
the 6 and 3 similarity is : 0.743699
the 6 and 5 similarity is : 0.468366
the 6 and 10 similarity is : 0.439465
the 7 and 3 similarity is : 0.482175
the 7 and 5 similarity is : 0.494716
the 7 and 10 similarity is : 0.524970
the 8 and 3 similarity is : 0.491307
the 8 and 5 similarity is : 0.491228
the 8 and 10 similarity is : 0.520290
the 9 and 3 similarity is : 0.522379
the 9 and 5 similarity is : 0.496130
the 9 and 10 similarity is : 0.493617
[(4, 3.3447149384692283), (7, 3.3294020724526967), (9, 3.328100876390069)]

尝试另外一种相似度计算方法:

>>> svdRec.recommend(myMat, 1, estMethod=svdRec.svdEst, simMeas=svdRec.pearsSim)
the 0 and 3 similarity is : 0.341942
the 0 and 5 similarity is : 0.124132
the 0 and 10 similarity is : 0.116698
the 1 and 3 similarity is : 0.345560
the 1 and 5 similarity is : 0.126456
the 1 and 10 similarity is : 0.118892
the 2 and 3 similarity is : 0.345149
the 2 and 5 similarity is : 0.126190
the 2 and 10 similarity is : 0.118640
the 4 and 3 similarity is : 0.450126
the 4 and 5 similarity is : 0.528504
the 4 and 10 similarity is : 0.544647
the 6 and 3 similarity is : 0.923822
the 6 and 5 similarity is : 0.724840
the 6 and 10 similarity is : 0.710896
the 7 and 3 similarity is : 0.319482
the 7 and 5 similarity is : 0.118324
the 7 and 10 similarity is : 0.113370
the 8 and 3 similarity is : 0.334910
the 8 and 5 similarity is : 0.119673
the 8 and 10 similarity is : 0.112497
the 9 and 3 similarity is : 0.566918
the 9 and 5 similarity is : 0.590049
the 9 and 10 similarity is : 0.602380
[(4, 3.3469521867021732), (9, 3.3353796573274699), (6, 3.307193027813037)]

可以在用其他多种相似度计算方法尝试,可以将这里的结果和前面的方法(不做SVD分解)进行比较,看看到底哪个性能更好。

5-3 构建推荐引擎面临的挑战

上述代码很好地展示了推荐引擎的工作流程以及SVD将数据映射为重要特征的过程。代码的可读性较好,但没有考虑执行效率。一个原因是,不必在每次估计评分时都做SVD分解。对于上述数据集,是否进行SVD分解在效率上没有太大的区别。但在更大规模的数据集上,SVD分解会降低程序的速度。SVD分解可在程序调入时运行一次。在大型系统中,SVD每天运行一次或者以更低频率运行,且还要离线运行。

搜索引擎中还存在其他很多规模扩展性的挑战性问题,比如矩阵的表示方法。在上面例子中有很多0,实际系统中0的数目更多。我们可通过只存储非零元素来节省内存和计算开销?另一个潜在的计算资源浪费来自相似度得分。在程序中,每次需要一个推荐得分时,都要计算多个物品的相似度得分,这些得分记录是物品之间的相似度。因此在需要时,这些记录可以被另一个用户重复使用。在实际中,另一个普通的做法就是离线计算并保存相似度得分。

推荐引擎面临的另一个问题就是如何在缺乏数据时给出好的推荐。这称为冷启动(cold-start)问题,处理起来十分困难。这个问题的另一个说法是,用户不会喜欢一个无效的物品,而用户不喜欢的物品有无效。如果推荐只是一个可有可无的功能,那么问题倒也不大。但如果应用的成功与否和推荐的成功与否密切相关,那么问题就变得严重了。

冷启动问题的解决方案,就是将推荐看成是搜索问题。在内部表现上,不同的解决方法虽然有所不同,但对用户而言却都是透明的。为了将推荐看成搜索问题,我们可能要使用所需要推荐物品的属性。在餐馆菜肴的例子中,可通过各种标签来标记菜肴,比如素食、美式BBQ、价格很贵等。同时,也可将这些属性作为相似度计算所需要的数据,这被称为基于内容(content-based)的推荐。可能,基于内容的推荐并不如前面介绍的基于协同过滤的推荐效果好,但有它就是个良好的开始。

六、示例:基于SVD的图像压缩

接下来,是一个关于如何SVD应用于图像压缩的例子。通过可视化的方式,该例子使我们很容易就能看到SVD对数据近似的效果。在代码库中,包含了一张手写的数字图像。该图像在第二章使用过。原始图像大小是32x32=1024像素,我们能否使用更少的像素来表示这张图呢?如果能对图像进行压缩,那么就可以节省空间或带宽开销了。

可以使用SVD来对数据降维,从而实现图像的压缩。下面就会看到利用SVD的手写数字图像的压缩过程了。下面的程序包含了数字的读入和压缩代码。要了解最后的压缩效果,对压缩后的图像进行了重构。

# 图像压缩函数

# 用于打印矩阵,
# 由于矩阵含有浮点数,因此必须定义浅色和深色。这里通过一个阈值来界定。
# 该函数遍历所有的矩阵元素,当元素大于阈值时打印1,否则打印0
def printMat(intMat, thresh=0.8) :
    for i in range(32) :
        for k in range(32) :
            if float(intMat[i,k]) > thresh :
                print 1,
            else : print 0,
        print ' '

# 实现了图像的压缩。它允许基于任意给定的奇异值数目来重构图像。
def imgCompress(numSV=3, thresh=0.8) :
    # 构建一个列表myl
    myl = []
    # 打开文本文件,以数值方式读入字符
    for line in open("C:\\Python27\\ml\\testDigits\\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)
    # 对原始图像进行SVD分解并重构图像,通过将Sigma重构成SigRecon来实现
    U,Sigma,VT = la.svd(myMat)
    # Sigma是一个对角矩阵,需要建立一个全0矩阵,然后将前面的那些奇异值填充到对角线上。
    SigRecon = mat(zeros((numSV, numSV)))
    for k in range(numSV) :
        SigRecon[k,k] = Sigma[k]
    # 通过截断的U和VT矩阵,用SigRecon得到重构后的矩阵
    reconMat = U[:,:numSV]*SigRecon*VT[:numSV,:]
    print "******reconstructed matrix using %d singular values******" % numSV
    printMat(reconMat, thresh)

查看运行效果:

>>> reload(svdRec)
<module 'ml.svdRec' from 'C:\Python27\ml\svdRec.py'>
>>> svdRec.imgCompress(2)
******original matrix******
0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0
0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0
0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0
******reconstructed matrix using 2 singular values******
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 1 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 1 1 1 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

可知,只需两个奇异值就能相当精确地对图像实现重构。那么,到底需要多少个0-1的数字来重构图像呢? U VT 都是32x2的矩阵,两个奇异值。因此,总数字数目是64+64+2=130。和原数目1024相比,获得了几乎10倍的压缩比。

七、本章小结

SVD是一种强大的降维工具,可以利用SVD来逼近矩阵并从中提取重要特征。通过保留80%-90%的能量,就可以得到重要的特征并去掉噪声。SVD已经运用到多种应用中,其中一个成功的应用案例就是推荐引擎。

推荐引擎将物品推荐给用户,协同过滤是一种基于用户喜好或行为数据的推荐的实现方法。协同过滤的核心是相似度计算方法,有很多相似度计算方法都可以用于计算物品或用户之间的相似度。通过在低维空间下计算相似度,SVD提高了推荐引擎的效果。

在大规模数据集上,SVD的计算和推荐可能是一个很困难的工程问题。通过离线方式来进行SVD分解和相似度计算,是一种减少冗余计算和推荐所需时间的方法。

猜你喜欢

转载自blog.csdn.net/namelessml/article/details/52987113
今日推荐