本文收录在推荐系统专栏,专栏系统化的整理推荐系统相关的算法和框架,并记录了相关实践经验,所有代码都已整理至推荐算法实战集合(hub-recsys)。
目录
特征分解——>奇异值分解(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 |
用户1 | 3 | 5 | 1 | ||||
用户2 | 2 | 4 | |||||
用户3 | 4 | ||||||
用户4 | 2 | 1 | |||||
用户5 | 1 | 4 |
即对于一个M行(M个user),N列(N个item)的矩阵,我们的任务是要通过分析已有的数据(观测数据)来对未知数据进行预测,即这是一个矩阵补全(填充)任务。
3.2 SVD应用
3.2.1 traditional-SVD
此时可以将这个用户物品对应的𝑚×𝑛矩阵𝑀进行SVD分解,并通过选择部分较大的一些奇异值来同时进行降维,也就是说矩阵𝑀此时分解为:
从而可以使用矩阵的乘积对稀疏空缺物品的评分做预测,然后再对评分排序,将高评分的物品推荐给用户。
缺点:
- 评分矩阵不稠密:用全局平均值或者用用户物品平均值补全。
- 耗时巨大:userItem维度通常很大,SVD分解耗时巨大的。
- 分解方式比较单一,获取的特征的方式不够灵活,是否可以完全表征用户和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()