从零开始-Machine Learning学习笔记(38)-基于SVD的推荐系统

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/kabuto_hui/article/details/88621706

0. 前言

  本文总结于Peter Harrington的《Machine Learning in Action》的第14章-利用SVD简化数据。

1. 关于SVD与特征值分解

  SVD(Singular Value Decomposition, SVD)奇异值分解。在说及SVD之前,我们需要了解一下特征值分解。学过线性代数都知道,如果一个方阵可以拆成如下的形式:
A v = λ v Av = \lambda v
λ \lambda 称为特征值,而v称为特征向量,如果这n个特征向量线性无关,那么矩阵就可以分解为如下的形式:
A = W Σ W 1 A = W \Sigma W^{-1}
其中,W是由n个特征向量组成的矩阵, Σ \Sigma 是由特征值组成的对角阵。通常我们会将W的n个向量进行标准化,即 W i 2 = 1 W T W = 1 ||W_i||_2 = 1或者W^T W = 1 ,此时的n个特征向量为标准正交基,满足 W T W = I W^T W = I , 于是矩阵A就可以分解为:
A = W Σ W T A = W \Sigma W^T
这就是矩阵的特征值分解。
  这里我们注意到,特征值分解用于方阵,但是实际问题中,往往不是方阵,所以为了对非方阵的矩阵进行分解,就需要我们的SVD。这里我们假设矩阵A是一个m*n的矩阵。

  1. 如果 A A T AA^T ,则 A A T AA^T 是一个m*m的方阵,对这个方阵进行特征值分解有:
    ( A A T ) u i = λ i u i (AA^T)u_i = \lambda_i u_i
    会得到m个特征值和m个特征向量,这m个特征向量组成一个矩阵U,我们称之为左奇异矩阵
  2. 同理,如果 A T A A^T A ,则 A T A A^T A 是一个n*n的方阵,对这个方阵进行特征值分解有:
    ( A T A ) v i = λ i v i (A^T A)v_i = \lambda_i v_i
    会得到n个特征值和n个特征向量,这n个特征向量组成一个矩阵V,我们称之为右奇异矩阵
  3. 将右奇异值矩阵对应的特征值进行开方,得到的结果称为奇异值,将这些奇异值组成对角矩阵 Σ \Sigma

于是,一个非方阵 A m n A_{m*n} ,可以分解为:
A m n = U m m Σ m n V n n T A_{m*n} = U_{m*m}\Sigma_{m*n}V_{n*n}^T
这就是奇异值分解的原理。

2. 基于SVD的推荐系统

  假如我们要对某个用户喜欢的电影进行预测,推荐系统发现一部电影该用户还没有看过,它就会计算该电影与用户看过的电影之间的相似度,如果相似度高,那么系统就会认为该用户也喜欢这部电影。所以,基于SVD的推荐系统的第一件事就是定义相似度。在推荐系统的中,相似度的定义一般有三种方式:欧式距离、皮尔逊相关系数和余弦距离。

2. 1 相似度

(1) 基于欧式距离的相似度:
S i m i l a r i t y = 1 1 + d i s t a n c e Similarity = \frac{1}{1 + distance}
如果距离越远,则相似度越小;距离越近,相似度越高。用python可以写为:

from numpy import linalg as la
def ecludSim(inA,inB):
    # 计算基于欧式距离的相似度
    return 1.0/(1.0 + la.norm(inA - inB))

其中la.norm()计算范数,默认为2范数。

(2) 基于皮尔逊相关系数的相似度:
S i m i l a r i t y = 0.5 + 0.5 c o r r c o e f ( ) Similarity = 0.5 + 0.5 *corrcoef()

def pearsSim(inA,inB):
    # 计算基于皮尔逊相关系数的相似度
    if len(inA) < 3 : return 1.0
    return 0.5+0.5*corrcoef(inA, inB, rowvar = 0)[0][1]

(3) 基于余弦距离的相似度“:
S i m i l a r i t y = 0.5 + 0.5 c o s ( θ ) c o s ( θ ) = A B A B Similarity = 0.5 + 0.5 * cos(\theta) \\ cos(\theta) = \frac{A*B}{||A|| ||B||}

def cosSim(inA,inB):
    # 计算基于余弦cos距离的相似度
    num = float(inA.T*inB)
    denom = la.norm(inA)*la.norm(inB)
    return 0.5+0.5*(num/denom)

2.2 使用SVD对未打分的物品进行打分

先看代码:

def svdEst(dataMat, user, simMeas, item):
    '''
    采用SVD奇异值分解的评价函数
    :param dataMat:     数据矩阵
    :param user:        用户【行数】
    :param simMeas:     相似度计算函数
    :param item:        待评价的物品
    :return:
    '''
    # 获取物品的数量
    n = shape(dataMat)[1]
    simTotal = 0.0; ratSimTotal = 0.0
    # 奇异值分解
    U,Sigma,VT = la.svd(dataMat)
    # 选取包含90%能量的奇异值,在这里是前4个Sigma,构成对角矩阵
    Sig4 = mat(eye(4)*Sigma[:4]) #arrange Sig4 into a diagonal matrix
    # 将原数据转化到低维空间中
    xformedItems = dataMat.T * U[:,:5] * Sig4.I  #create transformed items

    for j in range(n):
        # 取出user对物品j的打分
        userRating = dataMat[user,j]
        # 如果打分为0或者j等于待打分物品 ,就跳过
        if userRating == 0 or j==item: continue
        # 计算物品j与物品item在低维空间中的相似度
        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
    # 返回关于物品item的打分
    else: return ratSimTotal/simTotal

(1)首先获取物品的数量,然后进行奇异值分解,选取包含90%能量以上的奇异值。即计算 Σ \Sigma 中前多少个奇异值之和能达到所有奇异值和的90%。这里选择的是5,这个5是基于测试数据:

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]]
# 对其进行奇异值分解有:
dataMat = mat(loadExData2())
U, Sigma, VT = la.svd(dataMat)
print('Sigma = ',Sigma)
print('Ratio = ',sum( Sigma[:5]) / sum(Sigma))

Sigma = [15.77075346 11.40670395 11.03044558 4.84639758 3.09292055 2.58097379
1.00413543 0.72817072 0.43800353 0.22082113 0.07367823]
Ratio = 0.9014360861521987

(2)然后将原数据矩阵转化到低维空间中:

xformedItems = dataMat.T * U[:,:5] * Sig4.I 

这句话很多人都不理解,为什么是这个样子的。我后来在机器学习实战的P264中代码对应的公式推导中找到了答案,其实就是一个很简单的线性变换:
A m n U m k Σ k n V n k T A_{m*n}\simeq U_{m*k}\Sigma_{k*n}V_{n*k}^T
其中 V n k V_{n*k} 就是A在低维空间中的映射。现在只需要反解出来就可以了:
U m k 1 A m n U m k 1 U m k Σ k n V n k T U k m A m n U k m U m k Σ k n V n k T U k m U k m A m n E k k Σ k n V n k T Σ k n 1 U k m A m n Σ k n 1 Σ k n V n k T V n k T = Σ k n 1 U k m A m n V n k = ( Σ k n 1 U k m A m n ) T = A m n T U m k Σ k n 1 U_{m*k}^{-1}A_{m*n}\simeq U_{m*k}^{-1}U_{m*k}\Sigma_{k*n}V_{n*k}^T \\ U_{k*m}A_{m*n}\simeq U_{k*m}U_{m*k}\Sigma_{k*n}V_{n*k}^T \\ 由于U_{k*m}是正交阵,有:\\ U_{k*m}A_{m*n}\simeq E_{k*k}\Sigma_{k*n}V_{n*k}^T \\ \Sigma_{k*n}^{-1}U_{k*m}A_{m*n}\simeq \Sigma_{k*n}^{-1}\Sigma_{k*n}V_{n*k}^T \\ 于是: V_{n*k}^T = \Sigma_{k*n}^{-1}U_{k*m}A_{m*n} \\ 两边同时转置: V_{n*k} = (\Sigma_{k*n}^{-1}U_{k*m}A_{m*n})^T=A_{m*n}^TU_{m*k}\Sigma_{k*n}^{-1}

(3)对于每一个物品,计算与物品item在低维空间的中的相似度,遇到user打分为0或者j==item时就跳过。将这些相似度进行累加,对相似度与rating的乘积也进行累加,最后返回两者的比值:
R a t i o p r e d = r a t S i m T o t a l s i m T o t a l = i s i R i i s i Ratio_pred = \frac{ratSimTotal}{simTotal} \\ = \frac{\sum_i s_iR_i}{\sum_i s_i}
这个比值其实就是预测用户user在物品item上的打分,这个结果就是将用户对于其他物品打分的分值进行加权平均。而这个权值就是相似度。

2.3 基于SVD的推荐函数

  有了上述的打分函数,我们只需要遍历某个user没有打分的物品,使用打分函数 s v d E s t ( ) svdEst() 进行打分,最后返回得分最高的N个物品,推荐给用户user。

def recommend(dataMat, user, N=3, simMeas=cosSim, estMethod=standEst):
    '''
    推荐函数
    :param dataMat:     数据集
    :param user:        待推荐用户
    :param N:           推荐的个数
    :param simMeas:     相似度函数
    :param estMethod:   打分函数
    :return:            返回N个推荐的物品及其预测的得分
    '''
    # 找到用户user没有打分的物品
    unratedItems = nonzero(dataMat[user, :].A == 0)[1] #find unrated items
    # 如果没有未评价的物品,直接返回
    if len(unratedItems) == 0: return 'you rated everything'
    itemScores = []
    # 对于用户user没有评价的每个物品
    for item in unratedItems:
        # 计算该物品的得分
        estimatedScore = estMethod(dataMat, user, simMeas, item)
        # 把该物品及其得分加入到list中
        itemScores.append((item, estimatedScore))
    # 按照得分进行排序,并返回前N个物品
    return sorted(itemScores, key=lambda jj: jj[1], reverse=True)[:N]

3. 基于SVD的图像压缩

  SVD除了在推荐系统上有着很好的应用,也能应用在图像压缩上。将图像矩阵进行奇异值分解SVD,然后提取能使能量大于90%的前N个奇异值极其对应的左右奇异值矩阵。原来很大的图像,就可以用简单的左奇异矩阵U和右奇异矩阵V以及奇异值 Σ \Sigma 来表示,最后通过三者的乘积就可以复原图片。

4. SVD的评价

优点: 简化数据,去除噪声点,提高算法的结果;
缺点: 数据的转换可能难以理解;
适用于数据类型:数值型
​ 通过SVD对数据的处理,我们可以使用小得多的数据集来表示原始数据集,这样做实际上是去除了噪声和冗余信息,以此达到了优化数据、提高结果的目的。

参考资料

奇异值分解(SVD)原理与在降维中的应用
奇异值分解(SVD)原理详解
机器学习实战的P264中代码对应的公式推导
机器学习实战——SVD(奇异值分解)

猜你喜欢

转载自blog.csdn.net/kabuto_hui/article/details/88621706
今日推荐