电影推荐系统代码详细解释

[python]  view plain  copy
  1.   
[python]  view plain  copy
  1. # -*- coding: utf8 -*-  
  2. ''''' 
  3. Created on 2015-06-22 
  4. @author: Lockvictor 
  5. '''  
  6. import sys, random, math  
  7. import os  
  8. from operator import itemgetter  
  9. random.seed(0)  
  10. class ItemBasedCF():  
  11.     ''''' TopN recommendation - ItemBasedCF '''  
  12.     def __init__(self):  
  13.         self.trainset = {}  
  14.         self.testset = {}  
  15.         #此处依然无法输出  
  16.         print("类型=",type(self.trainset))  
  17.   
  18.         self.n_sim_movie = 20#训练集用的电影数量  
  19.         self.n_rec_movie = 10#推荐电影数量  
  20.   
  21.         self.movie_sim_mat = {}#初始化为字典  
  22.         self.movie_popular = {}#初始化为字典  
  23.         self.movie_count = 0  
  24.   
  25.         print >> sys.stderr, 'Similar movie number = %d' % self.n_sim_movie  
  26.         print >> sys.stderr, 'Recommended movie number = %d' % self.n_rec_movie  
  27.   
  28.         #def __init__(self)解释  
  29.         #初始化7个变量,四个字典,三个整形  
  30.         #self代表this指针  
  31.         #意思是,这些变量是归这个类管辖的  
  32.         #__init__是构造函数,用来初始化,写法固定  
  33.  
  34.     @staticmethod  
  35.     def loadfile(filename):  
  36.         ''''' load a file, return a generator. '''  
  37.         print("loadfile filename=", filename)  
  38.         fp = open(filename, 'r')  
  39.         for i, line in enumerate(fp):  
  40.             yield line.strip('\r\n')  
  41.             if i % 100000 == 0:  
  42.                 print >> sys.stderr, 'loading %s(%s)' % (filename, i)  
  43.         fp.close()  
  44.         print >> sys.stderr, 'load %s succ' % filename  
  45.   
  46.     #def loadfile(filename)解释  
  47.     #这里的filename指的是ratings.dat  
  48.     #line代表数据集中每行的内容  
  49.     #i是个计数器,表示当前读到第几行了,每当读取到100000行的整数倍  
  50.     #输出语句报个信儿。  
  51.     #@staticmethod表示这个函数可以定义在类的外面  
  52.     #enumerate是为了配合i而存在的,  
  53.     #也就是说,这个for循环原本可以简化为:  
  54.     #for line in (fp)  
  55.     #yield line.strip('\r\n')  
  56.     #line.strip('\r\n')  
  57.     #表示删除每行的回车符的ASCII编码  
  58.     #yield是加强版的return,类似于C语言里面的升级版return  
  59.     #可以返回多个元素,这里估计是返回多个属性的意思吧  
  60. ##################################################  
  61.   
  62.     def generate_dataset(self, filename, pivot=0.7):  
  63.         ''''' load rating data and split it to training set and test set '''  
  64.         print("generate_data filename=",filename)  
  65.         trainset_len = 0  
  66.         testset_len = 0  
  67. ########################added by yuchi as follows###############################  
  68.         train_file = os.getcwd() + '/train.txt'#数据集分割后的得到的训练集  
  69.         output1 = open(train_file, 'w')  
  70.         test_file = os.getcwd() + '/test.txt'#数据集分割后得到的测试集  
  71.         output2 = open(test_file, 'w')  
  72. #########################added by yuchi above###############################  
  73.         for line in self.loadfile(filename):  
  74.             user, movie, rating, _ = line.split('::')  
  75.             # split the data by pivot  
  76.             if (random.random() < pivot):#待会儿需要改回来,用上面一句替换  
  77.                 self.trainset.setdefault(user, {})  
  78.                 self.trainset[user][movie] = int(rating)  
  79.                 #print("trainset[user][movie]=",trainset[user][movie])  
  80.                 trainset_len += 1#70% of all data  
  81. ########################added by yuchi above#######################  
  82.                 train_str = str(user) + ' ' + str(movie) + ' ' +  '%d' %self.trainset[user][movie] + '\n'#在前面加上 '%d' %是为了让数字转化为字符串  
  83.                 output1.write(train_str)  
  84. ########################added by yuchi above#######################  
  85.             else:  
  86.                 self.testset.setdefault(user, {})  
  87.                 self.testset[user][movie] = int(rating)  
  88.                 testset_len += 1#30% of all data  
  89.                 test_str = str(user) + ' ' + str(movie) + ' ' +  '%d' %self.testset[user][movie] + '\n'  
  90. ########################added by yuchi above#######################  
  91.                 output2.write(test_str)  
  92. ########################added by yuchi above#######################  
  93.         output1.close()  
  94.         output2.close()  
  95.         print >> sys.stderr, 'split training set and test set succ'  
  96.         print >> sys.stderr, 'train set = %s' % trainset_len  
  97.         print >> sys.stderr, 'test set = %s' % testset_len  
  98.         ########################下面是解释##################  
  99.         #def generate_dataset函数解释  
  100.         #    user, movie, rating, _ = line.split('::')  
  101.         #这里的双冒号是分隔符,用来获取属性,这里的单独的一个下划线“_”是一个变量名,代表ratings.txt中的第四个属性,Timestamp(时间戳)。  
  102.         #所以可以直接用print语句输出这个下划线变量。  
  103.         #这个函数中的filename也是指的是ratings.dat  
  104.         #这个函数既需要产生数据集,又需要产生测试集  
  105.         #因为在构造函数__init__中初始化了两个字典(字典其实就是C + +中的map类型)变量:  
  106.         #trainset和testset,他们分别表示训练集和测试集  
  107.         #所以在这里使用_len分别对这两个字典变量的容量进行初始化。  
  108.         #random.random()  
  109.         #生成0和1之间的随机浮点数float  
  110.         #由于random.random会随机生成浮点数,pivot设置为0.7, 也就是说,这个filename中  
  111.         #会有70%变成训练用数据集,30 % 变成测试用数据集。  
  112.         #那么哪些数据会成为那70 % 中的一部分, 哪些数据会成为30 % 的一部分呢?随机决定。  
  113.         #因此,在分割filename的时候:  
  114.         #也就是说:  
  115.         #一堆糖果,分给两个小朋友A和B,设定临界点为2,抛骰子,如果抛到1和2,一颗糖果归A;  
  116.         #如果抛到3~6,一颗糖果归B, 最后分成两堆。  
  117.         #########  
  118.         #70 % 的概率会执行if语句,变成训练用数据集  
  119.         #添加完后,用以下语句表示容量+1  
  120.         #trainset_len += 1  
  121.         ##########  
  122.         #30 % 的概率会执行else语句,成为测试集  
  123.         #添加完后,用以下语句表示容量+1  
  124.         #testset_len += 1  
  125.         ##########  
  126.         #其中  
  127.         #int(rating)用来数据格式转化  
  128.         #user, movie, rating, _ = line.split('::')与下面的  
  129.         #self.trainset.setdefault(user, {})  
  130.         #对应  
  131.         #单独的一个_表示这个属性本代码不关心,随便起个名字,占坑  
  132.         #for循环在执行每次循环时,都会得到新的一条数据,用user,movie和rating和_去获得这个数据中的四个属性  
  133.         #然后在trainset这个字典变量中建立映射关系。  
  134.         #self.trainset.setdefault(user, {})  
  135.         #表示对字典的新一项初始化。  
  136.         #self.trainset[user][movie] = int(rating)  
  137.         #表示索引变量是user、movie  
  138.         #索引值是rating。  
  139.         #总得来讲,也就是说,从ratings.txt的每行的四个属性中,获取三个属性,丢掉一个属性,来重新建立数据集中的一个项。  
  140.         #函数的功能,从ratings中筛选得到有用的属性,重新建立映射关系,一部分变成训练用数据集,一部分变成测试用数据集。  
  141. #-----------------------------------------------------------------------------  
  142.     def calc_movie_sim(self):#这个函数总共3个双重for循环  
  143.         ''''' calculate movie similarity matrix '''  
  144.         print >> sys.stderr, 'counting movies number and popularity...'  
  145.         for user, movies in self.trainset.iteritems():#训练集的前两两个属性就是用户和电影编号,利用for循环遍历整个测试集。  
  146.             #这里的movies表示某特定用户看过的所有电影,所以movies不是指一部电影,是一个集合  
  147.             for movie in movies:#  
  148.                 if movie not in self.movie_popular:# have been defined as map(dictionary)  
  149.                     self.movie_popular[movie] = 0#流行度指的是用户对电影的评价数量。  
  150.                 self.movie_popular[movie] += 1#这里没法直接写入txt,因为相同的电影,流行度刷新后,新的一行写入txt,旧的一行不会被删除  
  151. #这里的mouvie_popular在离开for循环以后得到的是两列属性,movieID和评价次数。  
  152.         print >> sys.stderr, 'count movies number and popularity succ'  
  153.         print("流行度初步计算结束")  
  154.         # save the total number of movies  
  155.         self.movie_count = len(self.movie_popular)#流行电影的容量  
  156.         print >> sys.stderr, 'total movie number = %d' % self.movie_count  
  157. #-------------------------------以上得到的是每部电影被评价的次数--------------------------------------  
  158.         # count co-rated users between items  
  159.         #movie_sim_mat是相似度矩阵的意思  
  160.         itemsim_mat = self.movie_sim_mat#movie_sim_mat已经在构造函数中进行初始化  
  161.         #同样地,itemsim_mat也是个字典,  
  162.         print >> sys.stderr, 'building co-rated users matrix...'  
  163.   
  164.         for user, movies in self.trainset.iteritems():  
  165.             for m1 in movies:  
  166.                 for m2 in movies:  
  167.                     if m1 == m2: continue#数据没清洗过的情况下使用  
  168.                     itemsim_mat.setdefault(m1,{})  
  169.                     itemsim_mat[m1].setdefault(m2,0)  
  170.                     itemsim_mat[m1][m2] += 1#被同一个用户评过分的两个不同电影,他们在相似度矩阵中+1  
  171.                     #注意,对itemsim_mat操作的同时,改变了movie_sim_mat  
  172.                     #也就是说,类似于C++中,itemsim_mat就是self.movie_sim_mat的别名  
  173.                     #注意,self.movie_sim_mat是对象中的成员,itemsim_mat不是  
  174.                     #注意,代码中只有self.movie_sim_mat,不存在movie_sim_mat  
  175.                     #注意,代码中只有itemsim_mat,不存在self.itemsim_mat  
  176.  ####################以上是相似度矩阵的"初步计算",没有使用很复杂的计算方法,后面还要进行计算,才能得到最终的相似度矩阵  
  177.                     # print >> sys.stderr, 'build co-rated users matrix succ'  
  178.         #物品的流行度即指有多少用户为某物品评分  
  179.         # calculate similarity matrix  
  180.         print("☆☆☆☆☆☆×××××××××××××☆☆☆☆☆☆☆"self.movie_sim_mat[movie].items())  
  181.         print >> sys.stderr, 'calculating movie similarity matrix...'  
  182.         simfactor_count = 0#控制程序运行进度输出的,没啥用  
  183.         PRINT_STEP = 2000000#控制程序运行进度输出的,没啥用  
  184.   
  185.         for m1, related_movies in itemsim_mat.iteritems():#注意,这里使用的是余弦相似度  
  186.             for m2, count in related_movies.iteritems():  
  187.                 itemsim_mat[m1][m2] = count / math.sqrt(  
  188.                         self.movie_popular[m1] * self.movie_popular[m2])  
  189.                 simfactor_count += 1  
  190.                 if simfactor_count % PRINT_STEP == 0:  
  191.                     print >> sys.stderr, 'calculating movie similarity factor(%d)' % simfactor_count  
  192.         print("☆☆☆☆☆☆×××××××××××××☆☆☆☆☆☆☆"self.movie_sim_mat[movie].items())  
  193.         print >> sys.stderr, 'calculate movie similarity matrix(similarity factor) succ'  
  194.         print >> sys.stderr, 'Total similarity factor number = %d' %simfactor_count  
  195.   
  196. # -----------------------------------------------------------------------------  
  197.   
  198.     def recommend(self, user):  
  199.         ''''' Find K similar movies and recommend N movies. '''  
  200.         K = self.n_sim_movie#在构造函数中已经定义和初始化  
  201.         N = self.n_rec_movie#在构造函数中已经定义和初始化,某特定用户将会被推荐的电影数量  
  202.         rank = {}  
  203.         watched_movies = self.trainset[user]  
  204.         #这里之所以有sort函数是为了推荐符合度最高的几个电影给用户  
  205.         for movie, rating in watched_movies.iteritems():#从数据集中提取某个用户看过的电影中的两个数据  
  206.             for related_movie, w in sorted(self.movie_sim_mat[movie].items(),key=itemgetter(1), reverse=True)[:K]:#从大到小排序  
  207.                 #上面的movie_sim_mat是个具备有3个属性的字典:两个相似的电影,以及他们的相似度,所以w是相似度的意思,related_movie是根据代码后面的[movie]得到的相关电影  
  208.                 #因为一行有许多属性,所以上面这句代码中items的意思是取得该属性所在行的其他所有属性  
  209.                 #由于movie_sim_mat中本来每行数据只有三个属性,由于这里使用了[movie]索引,所以得到剩下两个属性  
  210.                 #而上面这句代码后面使用了itemgetter(1),表示对所得到的两个属性,按照第2的属性(也就是相似度系数)进行排序  
  211.                 #reverse=true代表从大小排序,在代码中的意思是,在相似度矩阵中获取与movie这个变量相关的所有电影,并且按照相似度系数的大小从大到小排序  
  212.                 #最后[:K]:表示取得K个项  
  213.                 if related_movie in watched_movies:  
  214.                     continue#如果相关电影在已经看过的电影中,则跳过,进行下一轮循环(我想这应该是数据没有清洗导致的)  
  215.                 rank.setdefault(related_movie, 0)#这句话不属于上面的if的管辖范畴  
  216.                 rank[related_movie] += w * rating  
  217.         # return the N best movies  
  218.         # 以上双循环的意思是,对某用户看过的所有电影进行遍历,  
  219.         # 对于某个特定的已经看过的电影而言,便利相似度矩阵中所有和这个“已经看过的电影”相关的电影  
  220.         # 相关的电影的意思是,矩阵中都是aij中,i对应于movie,j对应于related_movie  
  221.         # self.movie_sim_mat[movie].items()会返回两个参数,第一个参数赋值给related_movie,  
  222.         # 第二个参数赋值给w,代表“movie”和“related_movie”这两个变量的相似度,相似度在前面已经计算得出  
  223.         # 他这里把权重系数去乘以评分次数,制造出一个参数w*rating,作为rank中排序的指标  
  224.         # 来计算与“已经看过的每个电影”相关的  
  225.         return sorted(rank.items(), key=itemgetter(1), reverse=True)[:N]  
  226.         # 这句return的意思是相当于excel中的排序,这里的itemgetter(1)表示按照rank中  
  227.         # 数据的第二项对rank中所有数据进行排序  
  228.         # 注意itemgetter(i)的括号中的序号i从0开始,代表第1项  
  229.         # 另外注意,这里rank虽然是字典,但是return返回的类型是list  
  230.   
  231.   
  232.     def evaluate(self):#这个是用来评价推荐的电影是否准确的。  
  233.         ''''' return precision, recall, coverage and popularity '''  
  234.         print >> sys.stderr,'Evaluation start...'  
  235.   
  236.         #############################  
  237.         N = self.n_rec_movie  
  238.         #  varables for precision and recall   
  239.         hit = 0  
  240.         rec_count = 0  
  241.         test_count = 0  
  242.         # varables for coverage  
  243.         all_rec_movies = set()  
  244.         # varables for popularity  
  245.         popular_sum = 0  
  246.         f = open("recommend.txt""w")  
  247.         for i, user in enumerate(self.trainset):#i对应enumerate,user对应测试集trainset  
  248.             if i % 500 == 0:  
  249.                 print >> sys.stderr, 'recommended for %d users' % i  
  250.             test_movies = self.testset.get(user, {})  
  251.             rec_movies = self.recommend(user)#这一句代表推荐结果,注意推荐结果的类型是list,不是dict(字典)  
  252.             recommend_str = str(user) + ' ' + str(rec_movies) + ' ' +'\n'  
  253.             f.write(str(recommend_str))  
  254.             #后面的这个for循环是用来评价推荐的电影是否准确的  
  255.             for movie, w in rec_movies:  
  256.                 if movie in test_movies:  
  257.                     hit += 1  
  258.                 all_rec_movies.add(movie)  
  259.                 popular_sum += math.log(1 + self.movie_popular[movie])  
  260.             ###################下面的属于外循环,不属于内循环#########################  
  261.             rec_count += N#no use  
  262.             test_count += len(test_movies)#no use  
  263.         f.close()  
  264.         precision = hit / (1.0 * rec_count)#no use  
  265.         recall = hit / (1.0 * test_count)#no use  
  266.         coverage = len(all_rec_movies) / (1.0 * self.movie_count)#no use  
  267.         popularity = popular_sum / (1.0 * rec_count)#no use  
  268.   
  269.         print >> sys.stderr, 'precision=%.4f\trecall=%.4f\tcoverage=%.4f\tpopularity=%.4f' \  
  270.                 % (precision, recall, coverage, popularity)  
  271.   
  272.   
  273. if __name__ == '__main__':  
  274.     ratingfile = 'ml-1m/ratings.dat'  
  275.     itemcf = ItemBasedCF()  
  276.     itemcf.generate_dataset(ratingfile)  
  277.     itemcf.calc_movie_sim()  
  278. itemcf.evaluate()#这个函数中出推荐结果  

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

数据集属性:

ratings.dat数据格式:
UserID::MovieID::Rating::Timestamp
1000209条数据


movies.dat数据格式:
MovieID::Title::Genres
3952部电影


users.dat数据格式:
UserID::Gender::Age::Occupation::Zip-code
6040条数据




☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆☆
数据集中的双冒号原因:
用来让代码识别一行中的各个属性
双冒号作为分隔符
在第代码的函数def generate_dataset里面
user, movie, rating, _ = line.split('::')



另外,movies.dat和users.dat的信息已经包含在ratings.dat中了。所以在理解代码时不用关注

在代码运行结束后,可以根据用户和针对用户推荐的电影的ID,回过头在movies.dat和users.dat中查询,这样就知道ID的具体含义了。(这里ID的意思既包括用户ID,也包括电影名称的ID)

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

代码是转载的,由于注释不清楚,所以我在这里写了份详细的解释,这里的数据集用的是ml-1m,在网上很多地方都有下载。

另外注意:这里采用的是余弦相似度,代码中相关部分可以与下面的图示完全对应上。



代码我进行了了一定程度的修改,包括把数据集分割后的结果,分别写到train.txt和test.txt两个文件上。

推荐结果最终被写入recommend文件。

结果分析和验证:


设ratings.txt中与用户ID:5988相关的电影集合为C

在train.txt中也找到用户ID:5988,设该文件中,与该用户相关的电影集合为B

设在test.txt与用户ID:5988相关的集合为D

在recommend.txt中找到用户ID:5988,设该文件中,用户相关的电影集合为A


则必有

D∪B=C

D∩B=∅

当实验结果中可以发现有

A∩B=∅

A∈D时,说明推荐成功

之所以是∈的关系,是因为代码中限定了只推荐10部电影。



附录:

--------------------------------setdefault详细用法-------------------------------------------------------------------------

这个函数非常好,其实主要是获取信息,如果获取不到的时候就按照他的参数设置该值。
>>> a={}  
>>> a['key']='123'  
>>> print (a)  
{'key': '123'}  
>>> print (a.setdefault('key','456'))  #显示a这个字典的'key'值的内容,因为字典有,所以不会去设置它  
123  
  
>>> print (a.setdefault('key1','456')) #显示a这个字典的'key1'值的内容,因为字典没有,所以设置为456了  
456  
>>> a  
{'key1': '456', 'key': '123'} 


总的而言,这个函数的意思是:
查得到就查,不准改
查不到就在字典中添加。


-------------------------------iteritems用法-------------------------------------------------------------------------------------------------------------------

dic = {'a':"hello",'b':"how",'c':"you"}


for i in dic.iteritems():print (i)
('a', 'hello')
('c', 'you')
('b', 'how')

猜你喜欢

转载自blog.csdn.net/wdr2003/article/details/80611962
今日推荐