用SVD对数据进行降维并进行协同过滤构建简易推荐系统
数据来源
https://archive.ics.uci.edu/ml/datasets/Anonymous+Microsoft+Web+Data
运行环境
python3.7、PyCharm 2018.2.4 (Community Edition)
思路
观察数据及其说明可以看出,训练数据和测试数据均有很多行,就训练数据而言,数据前几行是无关数据,剩下的以'A'、'C'和'V'开头的数据是有用的,其中以'A'开头的数据给出了页面的代号以及标识名和后缀,以'C'开头的数据表示了用户代号,以'V'开头的数据给出了用户访问过的页面,其中以'C'开头的数据和以'A'开头的数据一起出现,为一组。其中训练数据文件中有32711个用户数据,测试数据中有5000个用户数据,且两者共享一套页面系统,即对应的页面代号及其含义都是相同的,另外,训练数据和测试数据的用户没有重叠,即它们是完全不同的两个用户组。综上,可以基于训练数据,对测试数据的5000个用户,根据每个用户已经访问过的页面预测出他们下一步可能会访问的页面,考虑到用户数远大于页面数,所以采用基于页面的协同过滤的办法,找出和已经访问过的页面最相似的页面作为预测的页面,又考虑到训练集的用户页面矩阵是一个32711×298的稀疏矩阵,所以需要对其进行降维处理,这里采用SVD降维,将其转化为一个298×50的低维矩阵,再进行相似度计算,最终对每个用户都预测5个可能访问的页面,取前三个打印查看。
源代码
recommendation.py
# -*- coding: UTF-8 -*-
#Author:Yinli
import pickle
import numpy as np
from scipy.sparse import linalg as spla
from numpy import linalg as la
#读取并处理训练集
def loadTrainingData(filename, numOfUser):
#逐行读取文件
fr = open(filename, 'r', encoding='utf-8')
arrayOfLines = fr.readlines()
#初始化数据矩阵,298列中有少许空列,并未在数据中出现过
dataMat = np.zeros((numOfUser,298))
#初始化页面字典
VrootDict = {}
#用来记录当前的用户
currentUser = 0
#遍历每行数据
for i in range(len(arrayOfLines)):
#处理此行数据
arrayOfLines[i] = arrayOfLines[i].rstrip('\n')
currentList = arrayOfLines[i].split(',')
#若此行数据以'C'开头,则记录更新当前用户
if currentList[0] == 'C':
currentUser = int(currentList[2]) - 10001
#若此行数据以'V'开头,则将对应用户和对应页面置1,表示此用户访问过此页面
if currentList[0] == 'V':
dataMat[currentUser][int(currentList[1])-1000] = 1
#若此行数据以'A'开头,则将此行数据处理计入页面字典
if currentList[0] == 'A':
VrootDict[int(currentList[1])-1000] = currentList[3:5]
#返回训练数据矩阵和页面字典
return dataMat, VrootDict
#读取测试数据
def loadTestingData(filename, numOfUser):
#逐行读取文件
fr = open(filename, 'r', encoding='utf-8')
arrayOfLines = fr.readlines()
#初始化数据矩阵
dataMat = np.zeros((numOfUser, 298))
#记录当前用户id
currentUser = 0
#逐行处理数据
for i in range(len(arrayOfLines)):
#处理此行数据
arrayOfLines[i] = arrayOfLines[i].rstrip('\n')
currentList = arrayOfLines[i].split(',')
#若此行数据以'C'开头,则记录更新当前用户
if(currentList[0] == 'C'):
currentUser = int(currentList[2]) - 10001
#若此行数据以'V'开头,则将对应用户和对应页面置1,表示此用户访问过此页面
if(currentList[0] == 'V'):
dataMat[currentUser][int(currentList[1])-1000] = 1
#返回测试数据矩阵
return dataMat
#计算两个列向量的欧式距离(正则化)
def EculdSim(inA, inB):
return 1.0/(1.0+la.norm((inA-inB)))
#计算两个列向量的皮尔逊相关系数
def pearsSim(inA, inB):
if(len(inA) < 3):
return 1.0
else:
return 0.5+0.5*np.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)
#将数据矩阵进行SVD降维
#将原m*n矩阵转化为n*dimension维矩阵
def reduceDimension(dataMat, dimension = 50):
#用处理稀疏矩阵的sparse包对矩阵进行svd分解
U, sigma, VT = spla.svds(dataMat, dimension)
#因为svds得到的奇异值和对应U和VT是逆序排序的,所以要将它们反过来
U[:, :dimension] = U[:, dimension - 1::-1]
sigma = sigma[::-1]
VT[:dimension, :] = VT[dimension - 1::-1, :]
#利用sigma一维数组构造奇异值矩阵
sig = np.mat(np.eye(dimension)*sigma)
#计算降维后的矩阵并返回
formedDataMat = np.dot(np.dot(dataMat.T, U[:, :dimension]), sig.I)
return formedDataMat
#基于已有数据对指定用户和指定页面进行预测
#根据用户访问过的页面计算指定页面与这些页面的相似度之和
def svdEst(testingDataMat, formedDataMat, user, simMeas, page):
n = np.shape(formedDataMat)[0]
simTotal = 0.0
#遍历每个页面
for j in range(n):
#如果要预测的用户访问过这个页面
userVisiting = testingDataMat[user, j]
#或者这个页面和要预测的页面一致,则遍历下个页面
if userVisiting == 0 or j == page:
continue
#计算这个页面和要预测的指定页面的相似度
similarity = simMeas(formedDataMat[page,:].T, formedDataMat[j,:].T)
#累加所有此用户访问过的非预测页面与要预测页面的相似度
simTotal += similarity
if simTotal == 0:
return 0
else:
return simTotal
#基于训练数据对测试矩阵进行预测,指定预测页面数
def recommend(trainingDataMat, testingDataMat, N = 5, simMeas = EculdSim, estMethod = svdEst):
m,n = np.shape(testingDataMat)
#用recomMMat记录给每个用户推荐的页面
recommendMat = np.zeros((m, N))
#将训练矩阵转换为n*50的矩阵
formedDataMat = reduceDimension(trainingDataMat)
#遍历每个用户
for i in range(m):
#用列表记录每个页面的预测值
pageScores = []
#预测每个页面
for j in range(n):
estimatedScore = estMethod(testingDataMat, formedDataMat, i, simMeas, j)
pageScores.append((j, estimatedScore))
#对预测结果排序,从大到小
sortedPageScores = sorted(pageScores, key=lambda jj:jj[1], reverse=True)
#记录前N个推荐页面
for k in range(N):
recommendMat[i][k] = sortedPageScores[k][0]
#打印结果追踪过程
print("第%d个用户的预测页面为:" % i)
print(recommendMat[i])
#返回预测结果
return recommendMat
#保存结果
def store(filename, result):
with open(filename, 'wb') as fw:
pickle.dump(result,fw)
#主函数
if __name__ == '__main__':
#读取数据
trainingFilename = r"D:\python_things\code\第7次作业\SVD数据集\anonymous-msweb.data"
testingFilename = r"D:\python_things\code\第7次作业\SVD数据集\anonymous-msweb.test"
trainingDataMat, vrootDict = loadTrainingData(trainingFilename, 32711)
testingDataMat = loadTestingData(testingFilename, 5000)
#计算预测结果矩阵
recommendMat = recommend(trainingDataMat, testingDataMat)
#将预测结果和页面字典保存到文件
resultFilename = r'D:\python_things\code\第7次作业\SVD数据集\resultMat'
vrootFilename = r'D:\python_things\code\第7次作业\SVD数据集\vrootDict'
store(resultFilename, recommendMat)
store(vrootFilename, vrootDict
displayResult.py
# -*- coding: UTF-8 -*-
#Author:Yinli
import pickle
import numpy as np
#从文件中读取保存的结果
def load(filename):
fr = open(filename, 'rb')
return pickle.load(fr)
#主函数
if __name__ == "__main__":
#从文件中读取预测结果和页面字典
resultFilename = r'D:\python_things\code\第7次作业\SVD数据集\resultMat'
resultMat = load(resultFilename)
vrootFilename = r'D:\python_things\code\第7次作业\SVD数据集\vrootDict'
vrootDict = load(vrootFilename)
#展示为每个用户推荐的页面
m,n = np.shape(resultMat)
print("对于输入的测试集,每个用户根据点击过的页面推荐三个可能会访问的页面:")
#遍历每个 用户
for i in range(m):
print("给编号为%d的用户推荐如下页面:" % (i + 10001))
#用count记录已推荐页面数
count = 0
#从前5个推荐页面中选有效的3个页面推荐
for j in range(5):
#如果推荐页面是有效的则推荐并计数加一
#因为有些列是空列,即不在页面记录中,所以要进行过滤
if(resultMat[i][j] in vrootDict.keys()):
print(vrootDict[resultMat[i][j]][0])
count += 1
#如果已经推荐了三个页面则退出此次循环
if count == 3:
break
运行结果
程序运行结果(部分):