您的位置:首页 > 其它

使用SVD方法实现电影推荐系统

2013-04-19 10:34 579 查看
这学期选了一门名叫《web智能与社会计算》的课,老师最后偷懒,最后的课程project作业直接让我们参加百度的一个电影推荐系统算法大赛,然后以在这个比赛中的成绩作为这门课大作业的成绩。不过,最终的结果并不需要百度官方的评估,只需要我们的截图即可(参看百度云平台),例如下面这个:



上面最重要的就是RMSE的数值,数值越小代表偏差越小,百度排行榜就是按值从小到大来排列的,这些人使用的可能是比SVD更好的算法,即使这样达到一定范围后再想进步就很难了,估计不会有人低于0.6这个值。



言归正传,下面来说说针对百度这个比赛如何如何用SVD来实现推荐系统,为了了解基本原理可以看看这篇文章:推荐系统相关算法(1):SVD (后面提到的三篇论文也值得一读)

1、数据预处理

本课程的要求是只完成任务一即可,做任务一的时候只需要用到两个数据集,一个是训练数据training_set,一个是待预测的数据predict,百度的要求如下:

l  任务一

n training_set.txt用户评分数据共三列,从左到右依次为userId、movieId、rating。即用户id、电影id、该用户对该电影的评分。列之间以’\t’分隔,行之间以’\r\n’分隔。

n predict.txt为预测集合,共两列。从左到右依次是userId,movieId。即用户id,电影id.列之间以’\t’分隔,行之间以’\r\n’分隔。参赛者需要预测出第三列,即该用户对该电影的评分,作为第三列,并提交给评测平台。需要注意的是,参赛者最终提的predict.txt是三列,列之间以’\t’分隔,行之间以’\r\n’分隔。行之间的顺序不能乱,行的总数不能少。
下载下来以后发现数据并不是从0开始计数的,training_set格式如下:
7245481 962729 4.0

7245481 356405 4.0

7245481 836383 4.0

7245481 284550 4.0

7245481 723581 4.0

7245481 827305 4.0

7245481 572786 4.0

7245481 473690 4.0
.....................
.....................
.....................

predict集数据格式如下:
7245481 794171

7245481 381060

7245481 776002

7245481 980705

7245481 354292

7245481 738735

7245481 624561

7245481 985808

7245481 378349

7245481 778269

7245481 242057
...................
...................
...................

在使用SVD的时候需要用数组存储每一个用户和每一个电影,这里用户的ID都是7位,电影的都是6位,如果直接开个七八位大数组来存储内存估计不够,而且还浪费空间,为此需要先把用户和电影ID进行映射到从0开始一个较小的范围内。
userMap = {}
movieMap = {}

with open('training_set.txt') as fp:
fp_user = open('usermap.txt', 'w')
fp_movis = open('moviemap.txt', 'w')
fp_out = open('smallMatrix.txt', 'w')
fp_prediction = open('test.txt', 'r')
fp_out2 = open('smallPredictionMatrix.txt','w')

for line in fp:
line = line.strip()
if line == '':
continue
tup = line.split()
raw_user = tup[0]
raw_movie = tup[1]
rate = float(tup[2])
if raw_user not in userMap:
userMap[raw_user] = len(userMap.keys())
user_id = userMap[raw_user]
if raw_movie not in movieMap:
movieMap[raw_movie] = len(movieMap.keys())
movie_id = movieMap[raw_movie]
fp_out.write('{0} {1} {2}\n'.format(user_id, movie_id, rate))

for raw_user, user_id in userMap.items():
fp_user.write('{0} {1}\n'.format(raw_user, user_id))

for raw_movie, movie_id in movieMap.items():
fp_movis.write('{0} {1}\n'.format(raw_movie, movie_id))

for line2 in fp_prediction:
line2 = line2.strip()
if line2 == '':
continue
tup2 = line2.split()
raw_user2 = tup2[0]
raw_movie2 = tup2[1]
user_id2 = userMap[raw_user2]
movie_id2 = movieMap[raw_movie2]
fp_out2.write('{0} {1}\n'.format(user_id2, movie_id2))

上面代码实现了用户ID和电影ID的映射过程,实现思想很简单,来一个ID,看看之前是否存在过,如果存在用那个值替换,如果不存在新加入一个,这里是从0开始存储的,处理完以后得到四个文件,用户ID,电影ID的键值对,处理完的小范围训练集,处理完的小范围预测集。
小范围的训练集如下:

0 0 4.0
0 1 4.0
0 2 4.0
0 3 4.0
0 4 4.0
0 5 4.0
0 6 4.0
0 7 4.0
0 8 4.0
0 9 4.0
0 10 4.0
0 11 4.0
0 12 4.0
0 13 4.0
0 14 4.0
0 15 4.0
0 16 4.0
0 17 4.0
0 18 4.0
0 19 4.0
0 20 4.0
0 21 4.0
0 22 4.0
0 23 4.0
0 24 4.0
0 25 4.0
0 26 4.0
0 27 4.0
0 28 4.0
0 29 4.0
0 30 4.0
0 31 4.0
0 32 4.0
0 33 4.0
0 34 4.0
0 35 4.0
0 36 4.0
0 37 4.0...小范围预测集如下:
0 617
0 567
0 575
0 1211
0 1735
0 1255
0 620
0 795
0 890
0 706
0 599
0 1248
0 1651
0 621
0 1996
0 1003
0 2347...

经过这步处理,就不需要再开大数组存储,统计下来不同的用户数,电影数都不到一万个。

2、利用SVD进行训练

得到了小规模数据,修改svd.conf文件里面的值,这里avarageScore这个值需要自己计算后填入,userNum,itemNum是用户和电影数目的范围,后面几个值也并不是固定的,可以根据实际情况进行修改

3.579231 10000 10000 10 0.01 0.05
averageScore userNum itemNum factorNum learnRate regularization

然后运行svd.py文件即可,我们这里训练数据时迭代一次一般需要十五秒的时间,显然训练是很耗时的,为了简单就迭代了五次而已。

3、预测数据

预测数据其实就是矩阵的乘法运算,相比训练来说速度要快很多。这里我们参考上面那篇博客里面的代码,把训练和预测放在一起执行,最终代码如下:

import math
import random
import cPickle as pickle

#calculate the overall average
def Average(fileName):
fi = open(fileName, 'r')
result = 0.0
cnt = 0
for line in fi:
cnt += 1
arr = line.split()
result += int(arr[2].strip())
return result / cnt

def InerProduct(v1, v2):
result = 0
for i in range(len(v1)):
result += v1[i] * v2[i]

return result

def PredictScore(av, bu, bi, pu, qi):
pScore = av + bu + bi + InerProduct(pu, qi)
if pScore < 1:
pScore = 1
elif pScore > 5:
pScore = 5

return pScore

#def SVD(configureFile, testDataFile, trainDataFile, modelSaveFile):
def SVD(configureFile, trainDataFile, modelSaveFile):
#get the configure
fi = open(configureFile, 'r')
line = fi.readline()
arr = line.split()
averageScore = float(arr[0].strip())
userNum = int(arr[1].strip())
itemNum = int(arr[2].strip())
factorNum = int(arr[3].strip())
learnRate = float(arr[4].strip())
regularization = float(arr[5].strip())
fi.close()

bi = [0.0 for i in range(itemNum)]
bu = [0.0 for i in range(userNum)]
temp = math.sqrt(factorNum)
qi = [[(0.1 * random.random() / temp) for j in range(factorNum)] for i in range(itemNum)]
pu = [[(0.1 * random.random() / temp) for j in range(factorNum)] for i in range(userNum)]
print("initialization end\nstart training\n")

#train model
preRmse = 1000000.0
for step in range(5):
fi = open(trainDataFile, 'r')
for line in fi:
arr = line.split()
uid = int(arr[0].strip()) - 1
iid = int(arr[1].strip()) - 1
score = int(arr[2].strip())
prediction = PredictScore(averageScore, bu[uid], bi[iid], pu[uid], qi[iid])

eui = score - prediction

#update parameters
bu[uid] += learnRate * (eui - regularization * bu[uid])
bi[iid] += learnRate * (eui - regularization * bi[iid])
for k in range(factorNum):
temp = pu[uid][k] #attention here, must save the value of pu before updating
pu[uid][k] += learnRate * (eui * qi[iid][k] - regularization * pu[uid][k])
qi[iid][k] += learnRate * (eui * temp - regularization * qi[iid][k])
fi.close()
#learnRate *= 0.9
#curRmse = Validate(testDataFile, averageScore, bu, bi, pu, qi)
#print("test_RMSE in step %d: %f" %(step, curRmse))
#if curRmse >= preRmse:
# break
#else:
# preRmse = curRmse

#write the model to files
fo = file(modelSaveFile, 'wb')
pickle.dump(bu, fo, True)
pickle.dump(bi, fo, True)
pickle.dump(qi, fo, True)
pickle.dump(pu, fo, True)
fo.close()
print("model generation over")

#validate the model
def Validate(testDataFile, av, bu, bi, pu, qi):
cnt = 0
rmse = 0.0
fi = open(testDataFile, 'r')
for line in fi:
cnt += 1
arr = line.split()
uid = int(arr[0].strip()) - 1
iid = int(arr[1].strip()) - 1
pScore = PredictScore(av, bu[uid], bi[iid], pu[uid], qi[iid])

tScore = int(arr[2].strip())
rmse += (tScore - pScore) * (tScore - pScore)
fi.close()
return math.sqrt(rmse / cnt)

#use the model to make predict
def Predict(configureFile, modelSaveFile, testDataFile, resultSaveFile):
#get parameter
fi = open(configureFile, 'r')
line = fi.readline()
arr = line.split()
averageScore = float(arr[0].strip())
fi.close()

#get model
fi = file(modelSaveFile, 'rb')
bu = pickle.load(fi)
bi = pickle.load(fi)
qi = pickle.load(fi)
pu = pickle.load(fi)
fi.close()

#predict
fi = open(testDataFile, 'r')
fo = open(resultSaveFile, 'w')
for line in fi:
arr = line.split()
uid = int(arr[0].strip()) - 1
iid = int(arr[1].strip()) - 1
pScore = PredictScore(averageScore, bu[uid], bi[iid], pu[uid], qi[iid])
fo.write("%f\n" %pScore)
fi.close()
fo.close()
print("predict over")

if __name__ == '__main__':
configureFile = 'svd.conf'
trainDataFile = 'ml_data\\smallMatrix.txt'
testDataFile = 'ml_data\\smallPredictionMatrix.txt'
modelSaveFile = 'svd_model.pkl'
resultSaveFile = 'prediction.txt'

#print("%f" %Average("ua.base"))
SVD(configureFile, trainDataFile, modelSaveFile)
Predict(configureFile, modelSaveFile, testDataFile, resultSaveFile)



4、数据后处理

预测结果作为一列输出到一个单独的文件,按照百度的要求需要把结果插入到预测集的最后一列,用python处理一下就好

fp1 = open('predict.txt')
fp2 = open('prediction.txt')
fp_out = open('file3.txt', 'w')
for line1, line2 in zip(fp1, fp2):
line1 = line1.strip()
line2 = line2.strip()
fp_out.write('{0}\t{1}\n'.format(line1, line2))



最终我们就得到了可以提交到百度云平台上面进行评价的文件,格式如下:
7245481 794171 3.879440
7245481 381060 4.028262
7245481 776002 4.152251
7245481 980705 3.986217
7245481 354292 3.758884
7245481 738735 3.925804
7245481 624561 3.880905
7245481 985808 3.776078
7245481 378349 3.902128
7245481 778269 3.892242
7245481 242057 3.871258
7245481 648898 3.861340
7245481 171218 3.696469
7245481 897136 3.834176
7245481 572785 3.917795
7245481 518661 3.835075
7245481 544840 3.873519
7245481 131620 3.725185
7245481 600353 3.899684
7245481 865019 3.878535
..........................
..........................
..........................

评价结果就是博文最开始的那张图了。

5、改进和思考

显然我们还能够让RMSE值再小一些,其实一般来说svd在训练的时候还需要有一个测试数据来验证训练的好坏,但百度没有给测试数据,这里我们也没弄,如果有测试数据训练的效果可能会好一些。同时在训练时迭代次数的选择也有技巧,选择太少或太多效果都可能不好,需要自己把握,我们这里只迭代了五次显然不够。svd.conf里面最后三个参数的选择应该还存在技巧。也许还存在其他一些改进的方法,例如考虑到用户之间的关系这些,不过那个处理起来就有点复杂了,任务二貌似就要考虑到这一点。

总的来说,利用SVD仅仅迭代五次就能有这样的结果确实让人惊讶,想法简单但结果却不错,看来简单的并不意外着是不好,一些问题的完美解决往往蕴含在简单之中。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐