基於用戶的協同過濾推薦算法原理-附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()  

 

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