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

http://blog.csdn.net/zhaoxinfan/article/details/8821419

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

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

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

1、数据预处理

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

l  任务一

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

n predict.txt为预测集合,共两列。从左到右依次是userId,movieId。即用户id,电影id.列之间以’ ’分隔,行之间以’ ’分隔。参赛者需要预测出第三列,即该用户对该电影的评分,作为第三列,并提交给评测平台。需要注意的是,参赛者最终提的predict.txt是三列,列之间以’分隔,行之间以’ ’分隔。行之间的顺序不能乱,行的总数不能少。

下载下来以后发现数据并不是从0开始计数的,training_set格式如下:

  1. 7245481 962729  4.0  
  2.   
  3. 7245481 356405  4.0  
  4.   
  5. 7245481 836383  4.0  
  6.   
  7. 7245481 284550  4.0  
  8.   
  9. 7245481 723581  4.0  
  10.   
  11. 7245481 827305  4.0  
  12.   
  13. 7245481 572786  4.0  
  14.   
  15. 7245481 473690  4.0  
  16. .....................  
  17. .....................  
  18. .....................  


predict集数据格式如下:

  1. 7245481 794171  
  2.   
  3. 7245481 381060  
  4.   
  5. 7245481 776002  
  6.   
  7. 7245481 980705  
  8.   
  9. 7245481 354292  
  10.   
  11. 7245481 738735  
  12.   
  13. 7245481 624561  
  14.   
  15. 7245481 985808  
  16.   
  17. 7245481 378349  
  18.   
  19. 7245481 778269  
  20.   
  21. 7245481 242057  
  22. ...................  
  23. ...................  
  24. ...................  


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

[python] view plaincopyprint?
 
  1. userMap = {}  
  2. movieMap = {}  
  3.   
  4. with open('training_set.txt') as fp:  
  5.     fp_user = open('usermap.txt''w')  
  6.     fp_movis = open('moviemap.txt''w')   
  7.     fp_out = open('smallMatrix.txt''w')  
  8.     fp_prediction = open('test.txt''r')  
  9.     fp_out2 = open('smallPredictionMatrix.txt','w')  
  10.   
  11.     for line in fp:  
  12.         line = line.strip()  
  13.         if line == '':  
  14.             continue  
  15.         tup = line.split()  
  16.         raw_user = tup[0]  
  17.         raw_movie = tup[1]  
  18.         rate = float(tup[2])  
  19.         if raw_user not in userMap:  
  20.             userMap[raw_user] = len(userMap.keys())  
  21.         user_id = userMap[raw_user]  
  22.         if raw_movie not in movieMap:  
  23.             movieMap[raw_movie] = len(movieMap.keys())  
  24.         movie_id = movieMap[raw_movie]  
  25.         fp_out.write('{0} {1} {2} '.format(user_id, movie_id, rate))  
  26.   
  27.     for raw_user, user_id in userMap.items():  
  28.         fp_user.write('{0} {1} '.format(raw_user, user_id))  
  29.   
  30.     for raw_movie, movie_id in movieMap.items():  
  31.         fp_movis.write('{0} {1} '.format(raw_movie, movie_id))  
  32.   
  33.     for line2 in fp_prediction:  
  34.         line2 = line2.strip()  
  35.         if line2 == '':  
  36.             continue  
  37.         tup2 = line2.split()  
  38.         raw_user2 = tup2[0]  
  39.         raw_movie2 = tup2[1]  
  40.         user_id2 = userMap[raw_user2]  
  41.         movie_id2 = movieMap[raw_movie2]  
  42.         fp_out2.write('{0} {1} '.format(user_id2, movie_id2))  


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

小范围的训练集如下:

  1. 0 0 4.0  
  2. 0 1 4.0  
  3. 0 2 4.0  
  4. 0 3 4.0  
  5. 0 4 4.0  
  6. 0 5 4.0  
  7. 0 6 4.0  
  8. 0 7 4.0  
  9. 0 8 4.0  
  10. 0 9 4.0  
  11. 0 10 4.0  
  12. 0 11 4.0  
  13. 0 12 4.0  
  14. 0 13 4.0  
  15. 0 14 4.0  
  16. 0 15 4.0  
  17. 0 16 4.0  
  18. 0 17 4.0  
  19. 0 18 4.0  
  20. 0 19 4.0  
  21. 0 20 4.0  
  22. 0 21 4.0  
  23. 0 22 4.0  
  24. 0 23 4.0  
  25. 0 24 4.0  
  26. 0 25 4.0  
  27. 0 26 4.0  
  28. 0 27 4.0  
  29. 0 28 4.0  
  30. 0 29 4.0  
  31. 0 30 4.0  
  32. 0 31 4.0  
  33. 0 32 4.0  
  34. 0 33 4.0  
  35. 0 34 4.0  
  36. 0 35 4.0  
  37. 0 36 4.0  
  38. 0 37 4.0...  

小范围预测集如下:

  1. 0 617  
  2. 0 567  
  3. 0 575  
  4. 0 1211  
  5. 0 1735  
  6. 0 1255  
  7. 0 620  
  8. 0 795  
  9. 0 890  
  10. 0 706  
  11. 0 599  
  12. 0 1248  
  13. 0 1651  
  14. 0 621  
  15. 0 1996  
  16. 0 1003  
  17. 0 2347...  


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

2、利用SVD进行训练

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

  1. 3.579231 10000 10000 10 0.01 0.05  
  2. averageScore userNum itemNum factorNum learnRate regularization   


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

3、预测数据

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

[python] view plaincopyprint?
 
  1. import math  
  2. import random  
  3. import cPickle as pickle  
  4.   
  5.   
  6. #calculate the overall average  
  7. def Average(fileName):  
  8.     fi = open(fileName, 'r')  
  9.     result = 0.0  
  10.     cnt = 0  
  11.     for line in fi:  
  12.         cnt += 1  
  13.         arr = line.split()  
  14.         result += int(arr[2].strip())  
  15.     return result / cnt  
  16.   
  17.   
  18.   
  19. def InerProduct(v1, v2):  
  20.     result = 0  
  21.     for i in range(len(v1)):  
  22.         result += v1[i] * v2[i]  
  23.           
  24.     return result  
  25.   
  26.   
  27. def PredictScore(av, bu, bi, pu, qi):  
  28.     pScore = av + bu + bi + InerProduct(pu, qi)  
  29.     if pScore < 1:  
  30.         pScore = 1  
  31.     elif pScore > 5:  
  32.         pScore = 5  
  33.           
  34.     return pScore  
  35.   
  36.       
  37. #def SVD(configureFile, testDataFile, trainDataFile, modelSaveFile):  
  38. def SVD(configureFile, trainDataFile, modelSaveFile):  
  39.     #get the configure  
  40.     fi = open(configureFile, 'r')  
  41.     line = fi.readline()  
  42.     arr = line.split()  
  43.     averageScore = float(arr[0].strip())  
  44.     userNum = int(arr[1].strip())  
  45.     itemNum = int(arr[2].strip())  
  46.     factorNum = int(arr[3].strip())  
  47.     learnRate = float(arr[4].strip())  
  48.     regularization = float(arr[5].strip())  
  49.     fi.close()  
  50.       
  51.     bi = [0.0 for i in range(itemNum)]  
  52.     bu = [0.0 for i in range(userNum)]  
  53.     temp = math.sqrt(factorNum)  
  54.     qi = [[(0.1 * random.random() / temp) for j in range(factorNum)] for i in range(itemNum)]     
  55.     pu = [[(0.1 * random.random() / temp)  for j in range(factorNum)] for i in range(userNum)]  
  56.     print("initialization end start training ")  
  57.       
  58.     #train model  
  59.     preRmse = 1000000.0  
  60.     for step in range(5):  
  61.         fi = open(trainDataFile, 'r')     
  62.         for line in fi:  
  63.             arr = line.split()  
  64.             uid = int(arr[0].strip()) - 1  
  65.             iid = int(arr[1].strip()) - 1  
  66.             score = int(arr[2].strip())           
  67.             prediction = PredictScore(averageScore, bu[uid], bi[iid], pu[uid], qi[iid])  
  68.                   
  69.             eui = score - prediction  
  70.           
  71.             #update parameters  
  72.             bu[uid] += learnRate * (eui - regularization * bu[uid])  
  73.             bi[iid] += learnRate * (eui - regularization * bi[iid])   
  74.             for k in range(factorNum):  
  75.                 temp = pu[uid][k]   #attention here, must save the value of pu before updating  
  76.                 pu[uid][k] += learnRate * (eui * qi[iid][k] - regularization * pu[uid][k])  
  77.                 qi[iid][k] += learnRate * (eui * temp - regularization * qi[iid][k])  
  78.         fi.close()  
  79.         #learnRate *= 0.9  
  80.         #curRmse = Validate(testDataFile, averageScore, bu, bi, pu, qi)  
  81.         #print("test_RMSE in step %d: %f" %(step, curRmse))  
  82.         #if curRmse >= preRmse:  
  83.         #   break  
  84.         #else:  
  85.         #   preRmse = curRmse  
  86.                       
  87.     #write the model to files  
  88.     fo = file(modelSaveFile, 'wb')  
  89.     pickle.dump(bu, fo, True)  
  90.     pickle.dump(bi, fo, True)  
  91.     pickle.dump(qi, fo, True)  
  92.     pickle.dump(pu, fo, True)  
  93.     fo.close()  
  94.     print("model generation over")  
  95.       
  96. #validate the model  
  97. def Validate(testDataFile, av, bu, bi, pu, qi):  
  98.     cnt = 0  
  99.     rmse = 0.0  
  100.     fi = open(testDataFile, 'r')          
  101.     for line in fi:  
  102.         cnt += 1  
  103.         arr = line.split()  
  104.         uid = int(arr[0].strip()) - 1  
  105.         iid = int(arr[1].strip()) - 1  
  106.         pScore = PredictScore(av, bu[uid], bi[iid], pu[uid], qi[iid])  
  107.               
  108.         tScore = int(arr[2].strip())  
  109.         rmse += (tScore - pScore) * (tScore - pScore)  
  110.     fi.close()  
  111.     return math.sqrt(rmse / cnt)  
  112.   
  113.   
  114.       
  115.   
  116. #use the model to make predict  
  117. def Predict(configureFile, modelSaveFile, testDataFile, resultSaveFile):  
  118.     #get parameter  
  119.     fi = open(configureFile, 'r')  
  120.     line = fi.readline()  
  121.     arr = line.split()  
  122.     averageScore = float(arr[0].strip())  
  123.     fi.close()  
  124.       
  125.     #get model  
  126.     fi = file(modelSaveFile, 'rb')  
  127.     bu = pickle.load(fi)  
  128.     bi = pickle.load(fi)  
  129.     qi = pickle.load(fi)  
  130.     pu = pickle.load(fi)  
  131.     fi.close()  
  132.       
  133.     #predict  
  134.     fi = open(testDataFile, 'r')  
  135.     fo = open(resultSaveFile, 'w')  
  136.     for line in fi:  
  137.         arr = line.split()  
  138.         uid = int(arr[0].strip()) - 1  
  139.         iid = int(arr[1].strip()) - 1  
  140.         pScore = PredictScore(averageScore, bu[uid], bi[iid], pu[uid], qi[iid])  
  141.         fo.write("%f " %pScore)  
  142.     fi.close()  
  143.     fo.close()  
  144.     print("predict over")  
  145.               
  146.   
  147. if __name__ == '__main__':  
  148.     configureFile = 'svd.conf'  
  149.     trainDataFile = 'ml_data\smallMatrix.txt'  
  150.     testDataFile = 'ml_data\smallPredictionMatrix.txt'  
  151.     modelSaveFile = 'svd_model.pkl'  
  152.     resultSaveFile = 'prediction.txt'  
  153.       
  154.     #print("%f" %Average("ua.base"))  
  155.     SVD(configureFile, trainDataFile, modelSaveFile)  
  156.     Predict(configureFile, modelSaveFile, testDataFile, resultSaveFile)  
  157.       
  158.       
  159.       
  160.       
  161.       
  162.           



4、数据后处理

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

[python] view plaincopyprint?
 
  1. fp1 = open('predict.txt')  
  2. fp2 = open('prediction.txt')  
  3. fp_out = open('file3.txt''w')  
  4. for line1, line2 in zip(fp1, fp2):  
  5.     line1 = line1.strip()  
  6.     line2 = line2.strip()  
  7.     fp_out.write('{0} {1} '.format(line1, line2))  
  8.       


最终我们就得到了可以提交到百度云平台上面进行评价的文件,格式如下:

  1. 7245481 794171  3.879440  
  2. 7245481 381060  4.028262  
  3. 7245481 776002  4.152251  
  4. 7245481 980705  3.986217  
  5. 7245481 354292  3.758884  
  6. 7245481 738735  3.925804  
  7. 7245481 624561  3.880905  
  8. 7245481 985808  3.776078  
  9. 7245481 378349  3.902128  
  10. 7245481 778269  3.892242  
  11. 7245481 242057  3.871258  
  12. 7245481 648898  3.861340  
  13. 7245481 171218  3.696469  
  14. 7245481 897136  3.834176  
  15. 7245481 572785  3.917795  
  16. 7245481 518661  3.835075  
  17. 7245481 544840  3.873519  
  18. 7245481 131620  3.725185  
  19. 7245481 600353  3.899684  
  20. 7245481 865019  3.878535  
  21. ..........................  
  22. ..........................  
  23. ..........................  


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

5、改进和思考

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

总的来说,利用SVD仅仅迭代五次就能有这样的结果确实让人惊讶,想法简单但结果却不错,看来简单的并不意外着是不好,一些问题的完美解决往往蕴含在简单之中。

原文地址:https://www.cnblogs.com/DjangoBlog/p/3640918.html