基于用户的协同过滤推荐算法原理-附python代码实现

  在推荐系统众多方法中,基于用户的协同过滤推荐算法是最早诞生的,原理也较为简单。该算法1992年提出并用于邮件过滤系统,两年后1994年被 GroupLens 用于新闻过滤。一直到2000年,该算法都是推荐系统领域最著名的算法。

  本文简单介绍基于用户的协同过滤算法思想以及原理,最后基于该算法实现园友的推荐,即根据你关注的人,为你推荐博客园中其他你有可能感兴趣的人。基本思想

  俗话说物以类聚、人以群分,拿看电影这个例子来说,如果你喜欢《蝙蝠侠》、《碟中谍》、《星际穿越》、《源代码》等电影,另外有个人也都喜欢这些电影,而且他还喜欢《钢铁侠》,则很有可能你也喜欢《钢铁侠》这部电影。

  所以说,当一个用户 A 需要个性化推荐时,可以先找到和他兴趣相似的用户群体 G,然后把 G 喜欢的、并且 A 没有听说过的物品推荐给 A,这就是基于用户的系统过滤算法。原理

 根据上述基本原理,我们可以将基于用户的协同过滤推荐算法拆分为两个步骤:

1. 找到与目标用户兴趣相似的用户集合

2. 找到这个集合中用户喜欢的、并且目标用户没有听说过的物品推荐给目标用户

1. 发现兴趣相似的用户

  通常用 Jaccard 公式或者余弦相似度计算两个用户之间的相似度。设 N(u) 为用户 u 喜欢的物品集合,N(v) 为用户 v 喜欢的物品集合,那么 u 和 v 的相似度是多少呢:

      Jaccard 公式:

      余弦相似度:

      假设目前共有4个用户: A、B、C、D;共有5个物品:a、b、c、d、e。用户与物品的关系(用户喜欢物品)如下图所示:

      如何一下子计算所有用户之间的相似度呢?为计算方便,通常首先需要建立“物品—用户”的倒排表,如下图所示:

      然后对于每个物品,喜欢他的用户,两两之间相同物品加1。例如喜欢物品 a 的用户有 A 和 B,那么在矩阵中他们两两加1。如下图所示:

      计算用户两两之间的相似度,上面的矩阵仅仅代表的是公式的分子部分。以余弦相似度为例,对上图进行进一步计算:

      到此,计算用户相似度就大功告成,可以很直观的找到与目标用户兴趣较相似的用户。

2. 推荐物品

      首先需要从矩阵中找出与目标用户 u 最相似的 K 个用户,用集合 S(u, K) 表示,将 S 中用户喜欢的物品全部提取出来,并去除 u 已经喜欢的物品。对于每个候选物品 i ,用户 u 对它感兴趣的程度用如下公式计算:

      其中 rvi 表示用户 v 对 i 的喜欢程度,在本例中都是为 1,在一些需要用户给予评分的推荐系统中,则要代入用户评分。

      举个例子,假设我们要给 A 推荐物品,选取 K = 3 个相似用户,相似用户则是:B、C、D,那么他们喜欢过并且 A 没有喜欢过的物品有:c、e,那么分别计算 p(A, c) 和 p(A, e):

      看样子用户 A 对 c 和 e 的喜欢程度可能是一样的,在真实的推荐系统中,只要按得分排序,取前几个物品就可以了。

      在社交网络的推荐中,“物品”其实就是“人”,“喜欢一件物品”变为“关注的人”,这一节用上面的算法实现给我推荐 10 个园友。

1. 计算 10 名与我兴趣最相似的园友

      由于只是为我一个人做用户推荐,所以没必要建立一个庞大的用户两两之间相似度的矩阵了,与我兴趣相似的园友只会在这个群体产生:我关注的人的粉丝。除我自己之外,目前我一共关注了23名园友,这23名园友一共有22936个唯一粉丝,我对这22936个用户逐一计算了相似度,相似度排名前10的用户及相似度如下:

暱称

关注数量

共同数量

相似度

蓝枫叶1938

5

4

0.373001923296126

FBI080703

3

3

0.361157559257308

鱼非鱼

3

3

0.361157559257308

Lauce

3

3

0.361157559257308

蓝色蜗牛

3

3

0.361157559257308

shanyujin

3

3

0.361157559257308

Mr.Huang

6

4

0.340502612303499

对世界说你好

6

4

0.340502612303499

strucoder

28

8

0.31524416249564

Mr.Vangogh

4

3

0.312771621085612

2. 计算对推荐园友的兴趣度

      这10名相似用户一共推荐了25名园友,计算得到兴趣度并排序:

排序

暱称

兴趣度

1

wolfy

0.373001923296126

2

Artech

0.340502612303499

3

Cat Chen

0.340502612303499

4

WXWinter(冬)

0.340502612303499

5

DanielWise

0.340502612303499

6

一路前行

0.31524416249564

7

Liam Wang

0.31524416249564

8

usharei

0.31524416249564

9

CoderZh

0.31524416249564

10

博客园团队

0.31524416249564

11

深蓝色右手

0.31524416249564

12

Kinglee

0.31524416249564

13

Gnie

0.31524416249564

14

riccc

0.31524416249564

15

Braincol

0.31524416249564

16

滴答的雨

0.31524416249564

17

Dennis Gao

0.31524416249564

18

刘冬.NET

0.31524416249564

19

李永京

0.31524416249564

20

浪端之渡鸟

0.31524416249564

21

李涛

0.31524416249564

22

阿不

0.31524416249564

23

JK_Rush

0.31524416249564

24

xiaotie

0.31524416249564

25

Leepy

0.312771621085612

只需要按需要取相似度排名前10名就可以了,不过看起来整个列表的推荐质量都还不错!

 

具体代码实现:

#-*- coding: utf-8 -*-  
''''' 
Created on 2015-06-22 
 
@author: Lockvictor 
'''  
import sys  
import random  
import math  
import os  
from operator import itemgetter    
from collections import defaultdict   
random.seed(0)    
''''' 
users.dat 数据集  
用户id 用户性别 用户年龄 用户职业 用户所在地邮编 
1::F::1::10::48067 
2::M::56::16::70072 
3::M::25::15::55117 
 
movies.dat 数据集 
电影id 电影名称 电影类型  
250::Heavyweights (1994)::Children's|Comedy 
251::Hunted, The (1995)::Action 
252::I.Q. (1994)::Comedy|Romance 
 
ratings.dat 数据集 
用户id 电影id 用户评分  时间戳 
157::3519::4::1034355415 
157::2571::5::977247494 
157::300::3::977248224 
 
'''  
  
class UserBasedCF(object):  
    ''''' TopN recommendation - User Based Collaborative Filtering '''  
  
    # 构造函数,用来初始化  
    def __init__(self):  
        # 定义 训练集 测试集 为字典类型  
        self.trainset = {}  
        self.testset = {}  
        # 训练集用的相似用户数  
        self.n_sim_user = 20  
        # 推荐电影数量  
        self.n_rec_movie = 10  
          
        self.user_sim_mat = {}  
        # 表示电影的流行度,有一个看过该电影,流行度+1,没有人看过,流行度的值默认为0  
        self.movie_popular = {}  
        # 记录电影数量  
        self.movie_count = 0  
        # sys.stderr 是用来重定向标准错误信息的  
        print ('相似用户数目为 = %d' % self.n_sim_user, file=sys.stderr)  
        print ('推荐电影数目为 = %d' %  
               self.n_rec_movie, file=sys.stderr)  
   
    # 加载文件  
    @staticmethod  
    def loadfile(filename):  
        ''''' load a file, return a generator. '''  
        # 以只读的方式打开传入的文件  
        fp = open(filename, 'r')  
        # enumerate()为枚举,i为行号从0开始,line为值  
        for i, line in enumerate(fp):  
            # yield 迭代去下一个值,类似next()  
                # line.strip()用于去除字符串头尾指定的字符。  
            yield line.strip('\r\n')  
            # 计数  
            if i % 100000 == 0:  
                print ('loading %s(%s)' % (filename, i), file=sys.stderr)  
        fp.close()  
        # 打印加载文件成功  
        print ('load %s succ' % filename, file=sys.stderr)  
  
    # 划分训练集和测试集 pivot用来定义训练集和测试集的比例  
    def generate_dataset(self, filename, pivot=0.7):  
        ''''' load rating data and split it to training set and test set '''  
        trainset_len = 0  
        testset_len = 0  
        for line in self.loadfile(filename):  
            # 根据 分隔符 :: 来切分每行数据  
            user, movie, rating, _ = line.split('::')  
            # 随机数字 如果小于0.7 则数据划分为训练集  
            if random.random() < pivot:  
                # 设置训练集字典,key为user,value 为字典 且初始为空  
                self.trainset.setdefault(user, {})  
                # 以下省略格式如下,集同一个用户id 会产生一个字典,且值为他评分过的所有电影  
                #{'1': {'914': 3, '3408': 4, '150': 5, '1': 5}, '2': {'1357': 5}}  
                self.trainset[user][movie] = int(rating)  
                trainset_len += 1  
            else:  
                self.testset.setdefault(user, {})  
                self.testset[user][movie] = int(rating)  
                testset_len += 1  
        # 输出切分训练集成功  
        print ('划分数据为训练集和测试集成功!', file=sys.stderr)  
        # 输出训练集比例  
        print ('训练集数目 = %s' % trainset_len, file=sys.stderr)  
        # 输出测试集比例  
        print ('测试集数目 = %s' % testset_len, file=sys.stderr)  
    # 建立物品-用户 倒排表  
    def calc_user_sim(self):  
        ''''' calculate user similarity matrix '''  
        # build inverse table for item-users  
        # key=movieID, value=list of userIDs who have seen this movie  
        print ('构建物品-用户倒排表中,请等待......', file=sys.stderr)  
        movie2users = dict()  
  
        # Python 字典(Dictionary) items() 函数以列表返回可遍历的(键, 值) 元组数组  
        for user, movies in self.trainset.items():  
            for movie in movies:  
                # inverse table for item-users  
                if movie not in movie2users:  
                    # 根据电影id 构造set() 函数创建一个无序不重复元素集  
                    movie2users[movie] = set()  
                # 集合中值为用户id  
                # 数值形如  
                # {'914': {'1','6','10'}, '3408': {'1'} ......}  
                movie2users[movie].add(user)  
                # 记录电影的流行度  
                if movie not in self.movie_popular:  
                    self.movie_popular[movie] = 0  
                self.movie_popular[movie] += 1  
        print ('构建物品-用户倒排表成功', file=sys.stderr)  
  
        # save the total movie number, which will be used in evaluation  
        self.movie_count = len(movie2users)  
        print ('总共被操作过的电影数目为 = %d' % self.movie_count, file=sys.stderr)  
  
        # count co-rated items between users  
        usersim_mat = self.user_sim_mat  
  
        print ('building user co-rated movies matrix...', file=sys.stderr)  
        # 令系数矩阵 C[u][v]表示N(u)∩N(v) ,假如用户u和用户v同时属于K个物品对应的用户列表,就有C[u][v]=K  
        for   
          
        , users in movie2users.items():  
            for u in users:  
                usersim_mat.setdefault(u, defaultdict(int))  
                for v in users:  
                    if u == v:  
                        continue  
                    usersim_mat[u][v] += 1  
        print ('build user co-rated movies matrix succ', file=sys.stderr)  
  
        # calculate similarity matrix  
        print ('calculating user similarity matrix...', file=sys.stderr)  
        # 记录计算用户兴趣相似度的次数  
        simfactor_count = 0  
        # 计算用户兴趣相似度复杂度上限值  
        PRINT_STEP = 2000000  
        # 循环遍历usersim_mat 根据余弦相似度公式计算出用户兴趣相似度  
        for u, related_users in usersim_mat.items():  
            for v, count in related_users.items():  
                # 以下是公式计算过程  
                usersim_mat[u][v] = count / math.sqrt(  
                    len(self.trainset[u]) * len(self.trainset[v]))  
                #计数 并没有什么卵用  
                simfactor_count += 1  
                if simfactor_count % PRINT_STEP == 0:  
                    print ('calculating user similarity factor(%d)' %  
                           simfactor_count, file=sys.stderr)  
  
        print ('calculate user similarity matrix(similarity factor) succ',  
               file=sys.stderr)  
        print ('Total similarity factor number = %d' %  
               simfactor_count, file=sys.stderr)  
    # 根据用户给予推荐结果  
    def recommend(self, user):  
        '''''定义给定K个相似用户和推荐N个电影'''  
        K = self.n_sim_user  
        N = self.n_rec_movie  
        # 定义一个字典来存储为用户推荐的电影  
        rank = dict()  
        # 使用watched_movies来表示用户看过的电影列表,后续在做推荐电影需要排除掉用户看过的电影  
        watched_movies = self.trainset[user]  
        # sorted() 函数对所有可迭代的对象进行排序操作。 key 指定比较的对象 ,reverse=True 降序,这里user_sim_mat好像应该换成usersim_mat  
        for similar_user, similarity_factor in sorted(self.user_sim_mat[user].items(),  
                                                      key=itemgetter(1), reverse=True)[0:K]:  
            for movie in self.trainset[similar_user]:  
                # 判断 如果这个电影 该用户已经看过 则跳出循环  
                if movie in watched_movies:  
                    continue  
                # 记录用户对推荐的电影的兴趣度,这里的兴趣度也是根据该用户与推荐用户的相似度来定的  
                rank.setdefault(movie, 0)  
                rank[movie] += similarity_factor  
        # return the N best movies  
        return sorted(rank.items(), key=itemgetter(1), reverse=True)[0:N]  
  
    # 计算 准确率,召回率,覆盖率,流行度  
    def evaluate(self):  
  
        ''''' print evaluation result: precision, recall, coverage and popularity '''  
        print ('Evaluation start...', file=sys.stderr)  
  
        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  
  
        for i, user in enumerate(self.trainset):  
            if i % 500 == 0:  
                print ('recommended for %d users' % i, file=sys.stderr)  
            test_movies = self.testset.get(user, {})  
            rec_movies = self.recommend(user)  
            for movie, _ 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  
            test_count += len(test_movies)  
        # 计算准确度  
        precision = hit / (1.0 * rec_count)  
        # 计算召回率  
        recall = hit / (1.0 * test_count)  
        # 计算覆盖率  
        coverage = len(all_rec_movies) / (1.0 * self.movie_count)  
        #计算流行度  
        popularity = popular_sum / (1.0 * rec_count)  
  
        print ('precision=%.4f\trecall=%.4f\tcoverage=%.4f\tpopularity=%.4f' %  
               (precision, recall, coverage, popularity), file=sys.stderr)  
  
  
if __name__ == '__main__':  
    ratingfile = os.path.join('ml-1m', 'ratings.dat')  
    usercf = UserBasedCF()  
    usercf.generate_dataset(ratingfile)  
    usercf.calc_user_sim()  
  
    ''''' 
    以下为用户id 为 1688的用户推荐的电影 
    a = usercf.recommend("1688") 
    [('1210', 3.1260082382168055), ('2355', 3.0990860017403934), ('1198', 2.692208437663706), ('1527', 2.643102457311887), ('3578', 2.61895974438311), ('1376', 2.469905776632142), ('110', 2.4324588006133383), ('1372', 2.4307454264036528), ('1240', 2.424265305355254), ('32', 2.3926144836965966)] 
    '''  
    usercf.evaluate()  

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章