推荐系统 - 矩阵分解(SVD)原理和实战

本文收录在推荐系统专栏,专栏系统化的整理推荐系统相关的算法和框架,并记录了相关实践经验,所有代码都已整理至推荐算法实战集合(hub-recsys)

目录

一. 特征分解

1.1 特征求解:

1.2 标准化:

1.3 特征分解条件

二. SVD

2.1 定义

2.2 求解方法

2.3 相关特性 

2.4 SVD的python实现

2.5 SVD在PCA中的应用

三. 推荐系统中的SVD

3.1 问题定义

3.2 SVD应用

3.2.1 traditional-SVD

3.2.2 FunkSVD

3.2.3 BiasSVD

3.2.4 SVD++

3.3 矩阵分解推荐小结

3.4 SVD实现用户评分预测(MovieLens数据集)


特征分解——>奇异值分解(SVD)——>隐语义模型(LFM),三个算法在前者的基础上推导而成,按顺序先后出现。三者均用于矩阵降维。其中:特征分解可用于主成分分析。奇异值分解(SVD)和隐语义模型(LFM)可用于推荐系统中,将评分矩阵补全、降维。

一. 特征分解

1.1 特征求解:

特征分解是指将矩阵分解为由其特征值和特征向量表示的矩阵之积的方法,我们首先回顾下特征值和特征向量的定义:

上式中,λ是矩阵A的一个特征值,x是矩阵A的特征值λ对应的一个n维特征向量。站在特征向量的角度,特征向量的几何含义是:特征向量x通过方阵A变换,只缩放,方向不变。

求得A的n个特征值后,组成对角矩阵∑,A的特征分解就可以表示为:

其中U是n个特征向量组成的n×n维方阵,∑是这n个特征值为主对角线的n×n维方阵。

1.2 标准化:

一般我们会把U的这n个特征向量标准化(可使用施密特正交化方法),即满足||𝑤𝑖||2=1, 或者说𝑤𝑖𝑇𝑤𝑖=1。标准化后∑的𝑛个特征向量为标准正交基,满足∑𝑇∑=𝐼,即∑𝑇=∑−1, 也就是说∑为酉矩阵。这样我们的特征分解表达式可以写成

1.3 特征分解条件

  • A是一个𝑛x𝑛的方阵
  • 有𝑛个线性无关的特征向量。

根据上述条件,实对称矩阵一定可以进行特征分解。但是针对更一般的情况,由于特征分解矩阵A必须为方阵,对于行和列不相同的矩阵,应该如何分解,可以引出我们下文讨论的SVD。

 

二. SVD

2.1 定义

SVD也是对矩阵进行分解,但是和特征分解不同,SVD并不要求要分解的矩阵为方阵。假设我们的矩阵A是一个𝑚×𝑛的矩阵,那么我们定义矩阵A的SVD为:

其中U是一个𝑚×𝑚矩阵,Σ是一个𝑚×𝑛矩阵,除了主对角线上的元素以外全为0,主对角线上的每个元素都称为奇异值,V是一个𝑛×𝑛的矩阵。U和V都是酉矩阵,即满足𝑈𝑇𝑈=𝐼,𝑉𝑇𝑉=𝐼。

2.2 求解方法

那么我们如何求出SVD分解后的𝑈,Σ,𝑉,简单的方式是和转置矩阵相乘,获得方阵,然后再对方阵进行特征分解。

利用式(2-2)特征值分解,得到的特征矩阵即为𝑈;利用式(2-3)特征值分解,得到的特征矩阵即为𝑉;对Σ𝑇Σ或的特征值开方,可以得到所有的奇异值。

 

2.3 相关特性 

奇异值可以被看作成一个矩阵的代表值,或者说,奇异值能够代表这个矩阵的信息。当奇异值越大时,它代表的信息越多。也就是说,我们也可以用最大的k个的奇异值和对应的左右奇异向量来近似描述矩阵,并且SVD的求解可实现并行化,SVD的缺点是分解出的矩阵解释性往往不强,有点黑盒子的味道,不过这不影响它的使用。

 

 

2.4 SVD的python实现

矩阵数据读取

import numpy as np
import pandas as pd
from scipy.io import loadmat
 
# 读取数据,使用自己数据集的路径。
train_data_mat = loadmat("../data/train_data2.mat")
train_data = train_data_mat["Data"]
print(train_data.shape)

特征值分解

# 数据必需先转为浮点型,否则在计算的过程中会溢出,导致结果不准确
train_dataFloat = train_data / 255.0
# 计算特征值和特征向量
eval_sigma1,evec_u = np.linalg.eigh(train_dataFloat.dot(train_dataFloat.T))

计算奇异矩阵

#降序排列后,逆序输出
eval1_sort_idx = np.argsort(eval_sigma1)[::-1]
# 将特征值对应的特征向量也对应排好序
eval_sigma1 = np.sort(eval_sigma1)[::-1]
evec_u = evec_u[:,eval1_sort_idx]
# 计算奇异值矩阵的逆
eval_sigma1 = np.sqrt(eval_sigma1)
eval_sigma1_inv = np.linalg.inv(np.diag(eval_sigma1))
# 计算右奇异矩阵
evec_part_v = eval_sigma1_inv.dot((evec_u.T).dot(train_dataFloat))

上面的计算出的evec_u, eval_sigma1, evec_part_v分别为左奇异矩阵,所有奇异值,右奇异矩阵。

2.5 SVD在PCA中的应用

对于PCA降维而言,需要找到样本协方差矩阵𝑋𝑇𝑋的最大的d个特征向量,然后用这最大的d个特征向量张成的矩阵来做低维投影降维。可以看出,在这个过程中需要先求出协方差矩阵𝑋𝑇𝑋,当样本数多样本特征数也多的时候,这个计算量是很大的。

SVD也可以得到协方差矩阵𝑋𝑇𝑋最大的d个特征向量张成的矩阵,但是SVD有个好处,有一些SVD的实现算法可以不求先求出协方差矩阵𝑋𝑇𝑋,也能求出我们的右奇异矩阵𝑉V。也就是说,我们的PCA算法可以不用做特征分解,而是做SVD来完成。这个方法在样本量很大的时候很有效。

PCA仅仅使用了我们SVD的右奇异矩阵,没有使用左奇异矩阵。左奇异矩阵可以用于行数的压缩。相对的,右奇异矩阵可以用于列数即特征维度的压缩,也就是我们的PCA降维。    

 

三. 推荐系统中的SVD

推荐系统 - 概述和技术演进中,我们提到利用矩阵分解是Model-Based协同过滤是广泛使用的方法。

3.1 问题定义

在推荐系统中,我们常常遇到的问题是这样的,我们有很多用户和物品,也有少部分用户对少部分物品的评分,我们希望预测目标用户对其他未评分物品的评分,进而将评分高的物品推荐给目标用户。比如下面的用户物品评分表:

用户\物品物品1物品2物品3物品4物品5物品6物品7
用户13 5  1 
用户2 2    4
用户3   4   
用户4  2   1
用户51   4  

即对于一个M行(M个user),N列(N个item)的矩阵,我们的任务是要通过分析已有的数据(观测数据)来对未知数据进行预测,即这是一个矩阵补全(填充)任务。

3.2 SVD应用

3.2.1 traditional-SVD

此时可以将这个用户物品对应的𝑚×𝑛矩阵𝑀进行SVD分解,并通过选择部分较大的一些奇异值来同时进行降维,也就是说矩阵𝑀此时分解为:

从而可以使用矩阵的乘积对稀疏空缺物品的评分做预测,然后再对评分排序,将高评分的物品推荐给用户。

缺点:

  1. 评分矩阵不稠密:用全局平均值或者用用户物品平均值补全。
  2. 耗时巨大:userItem维度通常很大,SVD分解耗时巨大的。
  3. 分解方式比较单一,获取的特征的方式不够灵活,是否可以完全表征用户和item特征。

 

3.2.2 FunkSVD

FunkSVD用于解决 traditional-SVD 计算耗时大的问题,同时避免解决稀疏的问题,将期望矩阵𝑀分解成两个矩阵Q和P的乘积。

FunkSVD如何将矩阵𝑀分解为𝑃和𝑄呢?这里采用了线性回归的思想。我们的目标是让用户的评分和用矩阵乘积得到的评分残差尽可能的小,也就是说,可以用均方差作为损失函数,来寻找最终的𝑃和𝑄。借鉴线性回归的思想,通过最小化观察数据的平方来寻求最优的用户和项目的隐含向量表示。同时为了避免过度拟合(Overfitting)观测数据,加入正则项。

 

最终可通过梯度下降或者随机梯度下降法来寻求最优解。

    

3.2.3 BiasSVD

BiasSVD假设评分系统包括三部分的偏置因素:1. 用户有一些和物品无关的评分因素,称为用户偏置项(用户就喜欢打高分),2.物品也有一些和用户无关的评分因素,称为物品偏置项(山寨商品)

假设评分系统平均分为𝜇,第i个用户的用户偏置项为𝑏𝑖,而第j个物品的物品偏置项为𝑏𝑗,则加入了偏置项以后的优化目标函数如下所示:

通过迭代我们最终可以得到𝑃和𝑄,进而用于推荐。BiasSVD增加了一些额外因素的考虑,因此在某些场景会比FunkSVD表现好。

 

3.2.4 SVD++

SVD++算法在BiasSVD算法上进一步做了增强,这里它增加考虑用户的隐式反馈。

它是基于这样的假设:用户对于项目的历史评分记录或者浏览记录可以从侧面反映用户的偏好,比如用户对某个项目进行了评分,可以从侧面反映他对于这个项目感兴趣,同时这样的行为事实也蕴含一定的信息。其中N(i)为用户i所产生行为的物品集合;ys为隐藏的对于项目j的个人喜好偏置,是一个我们所要学习的参数;至于|N(i)|的负二分之一次方是一个经验公式。

 

3.3 矩阵分解推荐小结

矩阵分解用于推荐方法本身来说,它容易编程实现,实现复杂度低,预测效果也好,同时还能保持扩展性,小的推荐系统用矩阵分解应该是一个不错的选择。

但是当数据的特征和维度逐渐增多时,不再具备优势,无法引入更多的side-information,同时也并没有解决数据稀疏和冷启动问题。

 

3.4 SVD实现用户评分预测(MovieLens数据集)

'''
Version:1.0
Created on 2014-02-25
@Author:Dior
'''
 
import random
import math
import cPickle as pickle
 
class SVD():
    def __init__(self,allfile,trainfile,testfile,factorNum=10):
        #all data file
        self.allfile=allfile
        #training set file
        self.trainfile=trainfile
        #testing set file
        self.testfile=testfile
        #get factor number
        self.factorNum=factorNum
        #get user number
        self.userNum=self.getUserNum()
        #get item number
        self.itemNum=self.getItemNum()
        #learning rate
        self.learningRate=0.01
        #the regularization lambda
        self.regularization=0.05
        #initialize the model and parameters
        self.initModel()
    #get user number function
    def getUserNum(self):
        file=self.allfile
        cnt=0
        userSet=set()
        for line in open(file):
            user=line.split('\t')[0].strip()
            if user not in userSet:
                userSet.add(user)
                cnt+=1
        return cnt
    #get item number function
    def getItemNum(self):
        file=self.allfile
        cnt=0
        itemSet=set()
        for line in open(file):
            item=line.split('\t')[1].strip()
            if item not in itemSet:
                itemSet.add(item)
                cnt+=1
        return cnt
    #initialize all parameters
    def initModel(self):
        self.av=self.average(self.trainfile)
        self.bu=[0.0 for i in range(self.userNum)]
        self.bi=[0.0 for i in range(self.itemNum)]
        temp=math.sqrt(self.factorNum)
        self.pu=[[(0.1*random.random()/temp) for i in range(self.factorNum)] for j in range(self.userNum)]
        self.qi=[[0.1*random.random()/temp for i in range(self.factorNum)] for j in range(self.itemNum)]
        print "Initialize end.The user number is:%d,item number is:%d,the average score is:%f" % (self.userNum,self.itemNum,self.av)
     #train model  
    def train(self,iterTimes=100):
        print "Beginning to train the model......"
        trainfile=self.trainfile
        preRmse=10000.0
        for iter in range(iterTimes):
            fi=open(trainfile,'r')
            #read the training file
            for line in fi:
                content=line.split('\t')
                user=int(content[0].strip())-1
                item=int(content[1].strip())-1
                rating=float(content[2].strip())
                #calculate the predict score
                pscore=self.predictScore(self.av,self.bu[user],self.bi[item],self.pu[user],self.qi[item])
                #the delta between the real score and the predict score
                eui=rating-pscore
                
                #update parameters bu and bi(user rating bais and item rating bais)
                self.bu[user]+=self.learningRate*(eui-self.regularization*self.bu[user])
                self.bi[item]+=self.learningRate*(eui-self.regularization*self.bi[item])
                for k in range(self.factorNum):
                    temp=self.pu[user][k]
                    #update pu,qi
                    self.pu[user][k]+=self.learningRate*(eui*self.qi[user][k]-self.regularization*self.pu[user][k])
                    self.qi[item][k]+=self.learningRate*(temp*eui-self.regularization*self.qi[item][k])
                #print pscore,eui
            #close the file
            fi.close()
            #calculate the current rmse
            curRmse=self.test(self.av,self.bu,self.bi,self.pu,self.qi)
            print "Iteration %d times,RMSE is : %f" % (iter+1,curRmse)
            if curRmse>preRmse:
                break
            else:
                preRmse=curRmse
        print "Iteration finished!"
    #test on the test set and calculate the RMSE
    def test(self,av,bu,bi,pu,qi):
        testfile=self.testfile
        rmse=0.0
        cnt=0
        fi=open(testfile)
        for line in fi:
            cnt+=1
            content=line.split('\t')
            user=int(content[0].strip())-1
            item=int(content[1].strip())-1
            score=float(content[2].strip())
            pscore=self.predictScore(av,bu[user],bi[item],pu[user],qi[item])
            rmse+=math.pow(score-pscore,2)
        fi.close()
        return math.sqrt(rmse/cnt)
    #calculate the average rating in the training set
    def average(self,filename):
        result=0.0
        cnt=0
        for line in open(filename):
            cnt+=1
            score=float(line.split('\t')[2].strip())
            result+=score
        return result/cnt
    #calculate the inner product of two vectors
    def innerProduct(self,v1,v2):
        result=0.0
        for i in range(len(v1)):
            result+=v1[i]*v2[i]
        return result
    def predictScore(self,av,bu,bi,pu,qi):
        pscore=av+bu+bi+self.innerProduct(pu,qi)
        if pscore<1:
            pscore=1
        if pscore>5:
            pscore=5
        return pscore
    
if __name__=='__main__':
    s=SVD("data\\u.data","data\\ua.base","data\\ua.test")
    #print s.userNum,s.itemNum
    #print s.average("data\\ua.base")
    s.train()

 

  • 6
    点赞
  • 63
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
SVD矩阵分解(Singular Value Decomposition)是一种常见的线性代数方法,可用于矩阵分析、压缩、降维、数据挖掘等领域。在本文中,我们将讨论SVD矩阵分解的计算意义和展望。 1.计算意义 SVD矩阵分解可以将一个矩阵分解为三个矩阵的乘积,即A=UΣV^T,其中U和V是正交矩阵,Σ是对角矩阵。这个分解的计算意义在以下方面得到了体现: 1.1 压缩和降维 SVD矩阵分解可以用于数据压缩和降维。在实际应用中,我们经常遇到高维数据,这些数据中存在冗余信息,因此我们需要通过某种方法来压缩数据以减少计算成本和存储空间。通过SVD矩阵分解,我们可以将一个矩阵分解为较小的三个矩阵,从而达到数据压缩和降维的目的。 1.2 数据挖掘 SVD矩阵分解可以用于数据挖掘中。在数据挖掘中,我们需要从大量的数据中提取有用的信息。通过SVD矩阵分解,我们可以将数据转换为低维空间,从而发现数据中隐藏的模式和规律,进而进行分类、聚类等操作。 1.3 图像处理 SVD矩阵分解在图像处理中也有广泛的应用。通过将图像矩阵分解为三个矩阵的乘积,我们可以实现图像的压缩和降噪等操作。此外,SVD矩阵分解还可以用于图像的特征提取和识别等领域。 2.展望 SVD矩阵分解在计算机科学和应用数学等领域中有广泛的应用,在未来也将继续发挥重要作用。以下是一些SVD矩阵分解的展望: 2.1 高性能计算 随着计算机硬件和软件技术的不断发展,SVD矩阵分解的计算速度将得到进一步提高,这将有助于在更大规模的数据集上应用SVD矩阵分解。 2.2 深度学习 SVD矩阵分解深度学习中也有广泛的应用。通过将神经网络中的权重矩阵进行SVD分解,可以实现神经网络的压缩和加速,从而提高模型的性能和效率。 2.3 量子计算 SVD矩阵分解在量子计算中也有应用前景。量子计算具有高效、快速和安全等优势,SVD矩阵分解可以在量子计算中得到更加有效的应用。 综上所述,SVD矩阵分解在计算意义和应用前景上都有广泛的应用。在未来,随着技术的不断发展,SVD矩阵分解将继续发挥重要作用,并对相关领域的发展产生积极的推动作用。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值