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

先说句不太好听的:
电影推荐这种东西,除非是在电商或者大公司类的等相关的公司工作,或者学习研究需要,否则这种代码就不要看了,浪费时间。

道理很简单,一般小公司就那么可怜巴巴的一小堆客户,手指头数的过来的产品种类,推荐个啥?公司用不到,也就没必要学。

各种书籍中,凡是涉及推荐系统,除非你目标的公司是做这种岗位的,否则请直接跳过。

 

注:代码不是我写的,我只是尽可能多的做了注释,所以这个博客是转载。

# -*- coding: utf8 -*-
'''
Created on 2015-06-22
@author: Lockvictor
'''
import sys, random, math
import os
from operator import itemgetter
random.seed(0)
class ItemBasedCF():
    ''' TopN recommendation - ItemBasedCF '''
    def __init__(self):
        self.trainset = {}
        self.testset = {}
        #此处依然无法输出
        print("类型=",type(self.trainset))

        self.n_sim_movie = 20#训练集用的电影数量
        self.n_rec_movie = 10#推荐电影数量

        self.movie_sim_mat = {}#初始化为字典
        self.movie_popular = {}#初始化为字典
        self.movie_count = 0

        print >> sys.stderr, 'Similar movie number = %d' % self.n_sim_movie
        print >> sys.stderr, 'Recommended movie number = %d' % self.n_rec_movie

        #def __init__(self)解释
        #初始化7个变量,四个字典,三个整形
        #self代表this指针
        #意思是,这些变量是归这个类管辖的
        #__init__是构造函数,用来初始化,写法固定

    @staticmethod
    def loadfile(filename):
        ''' load a file, return a generator. '''
        print("loadfile filename=", filename)
        fp = open(filename, 'r')
        for i, line in enumerate(fp):
            yield line.strip('\r\n')
            if i % 100000 == 0:
                print >> sys.stderr, 'loading %s(%s)' % (filename, i)
        fp.close()
        print >> sys.stderr, 'load %s succ' % filename

    #def loadfile(filename)解释
    #这里的filename指的是ratings.dat
    #line代表数据集中每行的内容
    #i是个计数器,表示当前读到第几行了,每当读取到100000行的整数倍
    #输出语句报个信儿。
    #@staticmethod表示这个函数可以定义在类的外面
    #enumerate是为了配合i而存在的,
    #也就是说,这个for循环原本可以简化为:
    #for line in (fp)
    #yield line.strip('\r\n')
    #line.strip('\r\n')
    #表示删除每行的回车符的ASCII编码
    #yield是加强版的return,类似于C语言里面的升级版return
    #可以返回多个元素,这里估计是返回多个属性的意思吧
##################################################

    def generate_dataset(self, filename, pivot=0.7):
        ''' load rating data and split it to training set and test set '''
        print("generate_data filename=",filename)
        trainset_len = 0
        testset_len = 0
########################added by yuchi as follows###############################
        train_file = os.getcwd() + '/train.txt'#数据集分割后的得到的训练集
        output1 = open(train_file, 'w')
        test_file = os.getcwd() + '/test.txt'#数据集分割后得到的测试集
        output2 = open(test_file, 'w')
#########################added by yuchi above###############################
        for line in self.loadfile(filename):
            user, movie, rating, _ = line.split('::')
            # split the data by pivot
            if (random.random() < pivot):#待会儿需要改回来,用上面一句替换
                self.trainset.setdefault(user, {})
                self.trainset[user][movie] = int(rating)
                #print("trainset[user][movie]=",trainset[user][movie])
                trainset_len += 1#70% of all data
########################added by yuchi above#######################
                train_str = str(user) + ' ' + str(movie) + ' ' +  '%d' %self.trainset[user][movie] + '\n'#在前面加上 '%d' %是为了让数字转化为字符串
                output1.write(train_str)
########################added by yuchi above#######################
            else:
                self.testset.setdefault(user, {})
                self.testset[user][movie] = int(rating)
                testset_len += 1#30% of all data
                test_str = str(user) + ' ' + str(movie) + ' ' +  '%d' %self.testset[user][movie] + '\n'
########################added by yuchi above#######################
                output2.write(test_str)
########################added by yuchi above#######################
        output1.close()
        output2.close()
        print >> sys.stderr, 'split training set and test set succ'
        print >> sys.stderr, 'train set = %s' % trainset_len
        print >> sys.stderr, 'test set = %s' % testset_len
        ########################下面是解释##################
        #def generate_dataset函数解释
        #    user, movie, rating, _ = line.split('::')
        #这里的双冒号是分隔符,用来获取属性,这里的单独的一个下划线“_”是一个变量名,代表ratings.txt中的第四个属性,Timestamp(时间戳)。
        #所以可以直接用print语句输出这个下划线变量。
        #这个函数中的filename也是指的是ratings.dat
        #这个函数既需要产生数据集,又需要产生测试集
        #因为在构造函数__init__中初始化了两个字典(字典其实就是C + +中的map类型)变量:
        #trainset和testset,他们分别表示训练集和测试集
        #所以在这里使用_len分别对这两个字典变量的容量进行初始化。
        #random.random()
        #生成0和1之间的随机浮点数float
        #由于random.random会随机生成浮点数,pivot设置为0.7, 也就是说,这个filename中
        #会有70%变成训练用数据集,30 % 变成测试用数据集。
        #那么哪些数据会成为那70 % 中的一部分, 哪些数据会成为30 % 的一部分呢?随机决定。
        #因此,在分割filename的时候:
        #也就是说:
        #一堆糖果,分给两个小朋友A和B,设定临界点为2,抛骰子,如果抛到1和2,一颗糖果归A;
        #如果抛到3~6,一颗糖果归B, 最后分成两堆。
        #########
        #70 % 的概率会执行if语句,变成训练用数据集
        #添加完后,用以下语句表示容量+1
        #trainset_len += 1
        ##########
        #30 % 的概率会执行else语句,成为测试集
        #添加完后,用以下语句表示容量+1
        #testset_len += 1
        ##########
        #其中
        #int(rating)用来数据格式转化
        #user, movie, rating, _ = line.split('::')与下面的
        #self.trainset.setdefault(user, {})
        #对应
        #单独的一个_表示这个属性本代码不关心,随便起个名字,占坑
        #for循环在执行每次循环时,都会得到新的一条数据,用user,movie和rating和_去获得这个数据中的四个属性
        #然后在trainset这个字典变量中建立映射关系。
        #self.trainset.setdefault(user, {})
        #表示对字典的新一项初始化。
        #self.trainset[user][movie] = int(rating)
        #表示索引变量是user、movie
        #索引值是rating。
        #总得来讲,也就是说,从ratings.txt的每行的四个属性中,获取三个属性,丢掉一个属性,来重新建立数据集中的一个项。
        #函数的功能,从ratings中筛选得到有用的属性,重新建立映射关系,一部分变成训练用数据集,一部分变成测试用数据集。
#-----------------------------------------------------------------------------
    def calc_movie_sim(self):#这个函数总共3个双重for循环
        ''' calculate movie similarity matrix '''
        print >> sys.stderr, 'counting movies number and popularity...'
        for user, movies in self.trainset.iteritems():#训练集的前两两个属性就是用户和电影编号,利用for循环遍历整个测试集。
            #这里的movies表示某特定用户看过的所有电影,所以movies不是指一部电影,是一个集合
            for movie in movies:#
                if movie not in self.movie_popular:# have been defined as map(dictionary)
                    self.movie_popular[movie] = 0#流行度指的是用户对电影的评价数量。
                self.movie_popular[movie] += 1#这里没法直接写入txt,因为相同的电影,流行度刷新后,新的一行写入txt,旧的一行不会被删除
#这里的mouvie_popular在离开for循环以后得到的是两列属性,movieID和评价次数。
        print >> sys.stderr, 'count movies number and popularity succ'
        print("流行度初步计算结束")
        # save the total number of movies
        self.movie_count = len(self.movie_popular)#流行电影的容量
        print >> sys.stderr, 'total movie number = %d' % self.movie_count
#-------------------------------以上得到的是每部电影被评价的次数--------------------------------------
        # count co-rated users between items
        #movie_sim_mat是相似度矩阵的意思
        itemsim_mat = self.movie_sim_mat#movie_sim_mat已经在构造函数中进行初始化
        #同样地,itemsim_mat也是个字典,
        print >> sys.stderr, 'building co-rated users matrix...'

        for user, movies in self.trainset.iteritems():
            for m1 in movies:
                for m2 in movies:
                    if m1 == m2: continue#数据没清洗过的情况下使用
                    itemsim_mat.setdefault(m1,{})
                    itemsim_mat[m1].setdefault(m2,0)
                    itemsim_mat[m1][m2] += 1#被同一个用户评过分的两个不同电影,他们在相似度矩阵中+1
                    #注意,对itemsim_mat操作的同时,改变了movie_sim_mat
                    #也就是说,类似于C++中,itemsim_mat就是self.movie_sim_mat的别名
                    #注意,self.movie_sim_mat是对象中的成员,itemsim_mat不是
                    #注意,代码中只有self.movie_sim_mat,不存在movie_sim_mat
                    #注意,代码中只有itemsim_mat,不存在self.itemsim_mat
 ####################以上是相似度矩阵的"初步计算",没有使用很复杂的计算方法,后面还要进行计算,才能得到最终的相似度矩阵
                    # print >> sys.stderr, 'build co-rated users matrix succ'
        #物品的流行度即指有多少用户为某物品评分
        # calculate similarity matrix
        print("☆☆☆☆☆☆×××××××××××××☆☆☆☆☆☆☆", self.movie_sim_mat[movie].items())
        print >> sys.stderr, 'calculating movie similarity matrix...'
        simfactor_count = 0#控制程序运行进度输出的,没啥用
        PRINT_STEP = 2000000#控制程序运行进度输出的,没啥用

        for m1, related_movies in itemsim_mat.iteritems():#注意,这里使用的是余弦相似度
            for m2, count in related_movies.iteritems():
                itemsim_mat[m1][m2] = count / math.sqrt(
                        self.movie_popular[m1] * self.movie_popular[m2])
                simfactor_count += 1
                if simfactor_count % PRINT_STEP == 0:
                    print >> sys.stderr, 'calculating movie similarity factor(%d)' % simfactor_count
        print("☆☆☆☆☆☆×××××××××××××☆☆☆☆☆☆☆", self.movie_sim_mat[movie].items())
        print >> sys.stderr, 'calculate movie similarity matrix(similarity factor) succ'
        print >> sys.stderr, 'Total similarity factor number = %d' %simfactor_count

# -----------------------------------------------------------------------------

    def recommend(self, user):
        ''' Find K similar movies and recommend N movies. '''
        K = self.n_sim_movie#在构造函数中已经定义和初始化
        N = self.n_rec_movie#在构造函数中已经定义和初始化,某特定用户将会被推荐的电影数量
        rank = {}
        watched_movies = self.trainset[user]
        #这里之所以有sort函数是为了推荐符合度最高的几个电影给用户
        for movie, rating in watched_movies.iteritems():#从数据集中提取某个用户看过的电影中的两个数据
            for related_movie, w in sorted(self.movie_sim_mat[movie].items(),key=itemgetter(1), reverse=True)[:K]:#从大到小排序
                #上面的movie_sim_mat是个具备有3个属性的字典:两个相似的电影,以及他们的相似度,所以w是相似度的意思,related_movie是根据代码后面的[movie]得到的相关电影
                #因为一行有许多属性,所以上面这句代码中items的意思是取得该属性所在行的其他所有属性
                #由于movie_sim_mat中本来每行数据只有三个属性,由于这里使用了[movie]索引,所以得到剩下两个属性
                #而上面这句代码后面使用了itemgetter(1),表示对所得到的两个属性,按照第2的属性(也就是相似度系数)进行排序
                #reverse=true代表从大小排序,在代码中的意思是,在相似度矩阵中获取与movie这个变量相关的所有电影,并且按照相似度系数的大小从大到小排序
                #最后[:K]:表示取得K个项
                if related_movie in watched_movies:
                    continue#如果相关电影在已经看过的电影中,则跳过,进行下一轮循环(我想这应该是数据没有清洗导致的)
                rank.setdefault(related_movie, 0)#这句话不属于上面的if的管辖范畴
                rank[related_movie] += w * rating
        # return the N best movies
        # 以上双循环的意思是,对某用户看过的所有电影进行遍历,
        # 对于某个特定的已经看过的电影而言,便利相似度矩阵中所有和这个“已经看过的电影”相关的电影
        # 相关的电影的意思是,矩阵中都是aij中,i对应于movie,j对应于related_movie
        # self.movie_sim_mat[movie].items()会返回两个参数,第一个参数赋值给related_movie,
        # 第二个参数赋值给w,代表“movie”和“related_movie”这两个变量的相似度,相似度在前面已经计算得出
        # 他这里把权重系数去乘以评分次数,制造出一个参数w*rating,作为rank中排序的指标
        # 来计算与“已经看过的每个电影”相关的
        return sorted(rank.items(), key=itemgetter(1), reverse=True)[:N]
        # 这句return的意思是相当于excel中的排序,这里的itemgetter(1)表示按照rank中
        # 数据的第二项对rank中所有数据进行排序
        # 注意itemgetter(i)的括号中的序号i从0开始,代表第1项
        # 另外注意,这里rank虽然是字典,但是return返回的类型是list


    def evaluate(self):#这个是用来评价推荐的电影是否准确的。
        ''' return precision, recall, coverage and popularity '''
        print >> sys.stderr,'Evaluation start...'

        #############################
        N = self.n_rec_movie
        #  varables for precision and recall 
        hit = 0
        rec_count = 0
        test_count = 0
        # varables for coverage
        all_rec_movies = set()
        # varables for popularity
        popular_sum = 0
        f = open("recommend.txt", "w")
        for i, user in enumerate(self.trainset):#i对应enumerate,user对应测试集trainset
            if i % 500 == 0:
                print >> sys.stderr, 'recommended for %d users' % i
            test_movies = self.testset.get(user, {})
            rec_movies = self.recommend(user)#这一句代表推荐结果,注意推荐结果的类型是list,不是dict(字典)
            recommend_str = str(user) + ' ' + str(rec_movies) + ' ' +'\n'
            f.write(str(recommend_str))
            #后面的这个for循环是用来评价推荐的电影是否准确的
            for movie, w in rec_movies:
                if movie in test_movies:
                    hit += 1
                all_rec_movies.add(movie)
                popular_sum += math.log(1 + self.movie_popular[movie])
            ###################下面的属于外循环,不属于内循环#########################
            rec_count += N#no use
            test_count += len(test_movies)#no use
        f.close()
        precision = hit / (1.0 * rec_count)#no use
        recall = hit / (1.0 * test_count)#no use
        coverage = len(all_rec_movies) / (1.0 * self.movie_count)#no use
        popularity = popular_sum / (1.0 * rec_count)#no use

        print >> sys.stderr, 'precision=%.4f\trecall=%.4f\tcoverage=%.4f\tpopularity=%.4f' \
                % (precision, recall, coverage, popularity)


if __name__ == '__main__':
    ratingfile = 'ml-1m/ratings.dat'
    itemcf = ItemBasedCF()
    itemcf.generate_dataset(ratingfile)
    itemcf.calc_movie_sim()
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')

 

 

 

 

 

  • 15
    点赞
  • 4
    评论
  • 38
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页