《推荐系统实践》——基于物品的协同过滤算法(代码实现)

1、基础算法

基于物品的协同过滤算法(简称ItemCF)给用户推荐那些和他们以前喜欢的物品类似的物品。不过ItemCF不是利用物品的内容计算物品之间类似度,而是利用用户的行为记录。python

该算法认为,物品A和物品B具备很大的类似度是由于喜欢物品A的用户大都也喜欢物品B。这里蕴含一个假设,就是每一个用户的兴趣都局限在某几个方面,所以若是两个物品属于同一个用户的兴趣列表,那么这两个物品可能就属于有限的几个领域。而若是两个物品同时出如今不少用户的兴趣列表,那么它们可能就属于同一领域,于是具备很大的类似度。web

从上述概念出发,定义物品i和j的类似度为
物品类似度公式
其中, | N ( i ) | 是喜欢物品i的用户数, | N ( i ) N ( j ) | 是同时喜欢物品i和物品j的用户数。分母是惩罚物品i和j的权重,所以惩罚了热门物品和不少物品类似的可能性。算法

在获得物品类似度以后,ItemCF经过如下公式计算用户u对未产生行为的物品j的感兴趣程度。
推荐程度计算公式
这里的 N ( u ) 是用户喜欢的物品集合, S ( j , K ) 是和物品j最类似的K个物品的集合, w i j 是物品j和i的类似度, r u i 是用户u对物品j的兴趣评分(简单的,若是用户u对物品i有过行为,便可令 r u i =1)ruby

下面举一个例子说明(只考虑用户有历史购买行为的物品)。
用户A购买物品a b d,用户B购买物品b c e,用户C购买物品c d,用户D购买物品b c d,用户E购买物品a d。bash

user item
A a b d
B b c e
C c d
D b c d
E a d

数据集格式为(用户, r u i =1,物品),每行记录都是惟一的,兴趣评分由 r u i 决定。app

uid_score_bid = ['A,1,a','A,1,b','A,1,d','B,1,b','B,1,c','B,1,e','C,1,c','C,1,d','D,1,b','D,1,c','D,1,d','E,1,a','E,1,d']
import math  

class ItemBasedCF:  
    def __init__(self,train_file):  
        self.train_file = train_file  
        self.readData()  
    def readData(self):  
        #读取文件,并生成数据集(用户,兴趣程度,物品) 
        self.train = dict()       
        for line in self.train_file:  
            user,score,item = line.strip().split(",")  
            self.train.setdefault(user,{})  
            self.train[user][item] = int(float(score))  
         print (self.train) #输出数据集

    def ItemSimilarity(self): 
        C = dict()  #物品-物品的共现矩阵
        N = dict()  #物品被多少个不一样用户购买 
        for user,items in self.train.items():  
            for i in items.keys():  
                N.setdefault(i,0)  
                N[i] += 1  #物品i出现一次就计数加一
                C.setdefault(i,{})  
                for j in items.keys():  
                    if i == j : continue  
                    C[i].setdefault(j,0)  
                    C[i][j] += 1  #物品i和j共现一次就计数加一

        print ('N:',N) 
        print ('C:',C)

        #计算类似度矩阵 
        self.W = dict()  
        for i,related_items in C.items():  
            self.W.setdefault(i,{})  
            for j,cij in related_items.items():  
                self.W[i][j] = cij / (math.sqrt(N[i] * N[j]))  #按上述物品类似度公式计算类似度

        for k,v in self.W.items():
            print (k+':'+str(v))
        return self.W  

    #给用户user推荐前N个最感兴趣的物品 
    def Recommend(self,user,K=3,N=10):  
        rank = dict() #记录user的推荐物品(没有历史行为的物品)和兴趣程度
        action_item = self.train[user]     #用户user购买的物品和兴趣评分r_ui 
        for item,score in action_item.items():  
            for j,wj in sorted(self.W[item].items(),key=lambda x:x[1],reverse=True)[0:K]:  #使用与物品item最类似的K个物品进行计算
                if j in action_item.keys():  #若是物品j已经购买过,则不进行推荐
                    continue  
                rank.setdefault(j,0)  
                rank[j] += score * wj  #若是物品j没有购买过,则累计物品j与item的类似度*兴趣评分,做为user对物品j的兴趣程度
        return dict(sorted(rank.items(),key=lambda x:x[1],reverse=True)[0:N]) 

#声明一个ItemBased推荐的对象 
Item = ItemBasedCF(uid_score_bid) 
Item.ItemSimilarity()  
recommedDic = Item.Recommend("A")  #计算给用户A的推荐列表
for k,v in recommedDic.items():  
    print (k,"\t",v  )

输出结果:
数据集self.traindom

{'A': {'a': 1, 'd': 1, 'b': 1}, 'E': {'a': 1, 'd': 1}, 'D': {'d': 1, 'b': 1, 'c': 1}, 'B': {'e': 1, 'b': 1, 'c': 1}, 'C': {'d': 1, 'c': 1}}

物品被多少个不一样用户购买svg

N: {'e': 1, 'a': 2, 'd': 4, 'b': 3, 'c': 3}

物品-物品的共现矩阵测试

C: {'e': {'b': 1, 'c': 1}, 'a': {'d': 2, 'b': 1}, 'd': {'a': 2, 'b': 2, 'c': 2}, 'b': {'a': 1, 'd': 2, 'e': 1, 'c': 2}, 'c': {'e': 1, 'd': 2, 'b': 2}}

物品类似矩阵ui

a:{'d': 0.7071067811865475, 'b': 0.4082482904638631}
d:{'a': 0.7071067811865475, 'b': 0.5773502691896258, 'c': 0.5773502691896258}
e:{'b': 0.5773502691896258, 'c': 0.5773502691896258}
c:{'d': 0.5773502691896258, 'e': 0.5773502691896258, 'b': 0.6666666666666666}
b:{'a': 0.4082482904638631, 'd': 0.5773502691896258, 'e': 0.5773502691896258, 'c': 0.6666666666666666}

用户A的推荐列表

e    0.5773502691896258
c    1.2440169358562925

2、用户活跃度对物品类似度的影响

在ItemCF中,两个物品产生类似度是由于它们共同出如今不少用户的兴趣列表中。假设有这么一个用户,他是开书店的,而且买了当当网上 80% 的书准备用来本身卖。那么,
他的购物车里包含当当网 80% 的书。因此这个用户对于他所购买书的两两类似度的贡献应该远远小于一个只买了十几本本身喜欢的书的文学青年。
提出一个称为 IUF ( Inverse User Frequence ),即用户活跃度对数的倒数的参数,来修正物品类似度的计算公式。认为活跃用户对物品类似度的贡献应该小于不活跃的用户。
IUF

3、物品类似度的归一化

对于已经获得的物品类似度矩阵w,按照如下公式对w进行按列归一化,不只能够增长推荐的准确度,它还能够提升推荐的覆盖率和多样性。
归一化
假设物品分为两类—— A 和 B , A 类物品之间的类似度为 0.5 , B 类物品之间的类似度为 0.6 ,而 A 类物品和 B 类物品之间的类似度是 0.2 。在这种状况下,若是一个用户喜欢了 5个 A 类物品和 5 个 B 类物品,用 ItemCF 给他进行推荐,推荐的就都是 B 类物品,由于 B 类物品之间的类似度大。但若是归一化以后, A 类物品之间的类似度变成了 1 , B 类物品之间的类似度也是 1 ,那么这种状况下,用户若是喜欢 5 个 A 类物品和 5 个 B类物品,那么他的推荐列表中 A 类物品和 B 类物品的数目也应该是大体相等的。从这个例子能够看出,类似度的归一化能够提升推荐的多样性。
通常来讲,热门的类其类内物品类似度通常比较大。若是不进行归一化,就会推荐比较热门的类里面的物品,而这些物品也是比较热门的。所以,推荐的覆盖率就比较低。相反,若是进行类似度的归一化,则能够提升推荐系统的覆盖率。

结合二,三改进算法

def ItemSimilarity(self):  
        C = dict()  
        N = dict()  
        for user,items in self.train.items():  
            for i in items.keys():  
                N.setdefault(i,0)  
                N[i] += 1  
                C.setdefault(i,{})  
                for j in items.keys():  
                    if i == j : continue  
                    C[i].setdefault(j,0)  
                    #C[i][j] += 1 #基础算法
                    C[i][j] += 1/math.log(1+len(items)*1.0) #改进第一点

        print ('N:',N)
        print ('C:',C)

        #计算类似度矩阵 
        self.W = dict()  
        self.W_max = dict() #记录每一列的最大值
        for i,related_items in C.items():  
            self.W.setdefault(i,{})  

            for j,cij in related_items.items(): 
                self.W_max.setdefault(j,0.0)#
                self.W[i][j] = cij / (math.sqrt(N[i] * N[j]))  
                if self.W[i][j]>self.W_max[j]:#
                    self.W_max[j]=self.W[i][j] #记录第j列的最大值,按列归一化
        print('W:',self.W)
        for i,related_items in C.items():  #
            for j,cij in related_items.items(): #
                self.W[i][j]=self.W[i][j] / self.W_max[j] #

        print ('W_max:',self.W_max)
        for k,v in self.W.items():
            print (k+':'+str(v))

        return self.W

输出结果:
物品类似度矩阵W(归一化以前)

W: {'a': {'b': 0.2944888920518062, 'd': 0.576853026474115}, 'c': {'b': 0.4808983469629878, 'd': 0.470998523813926, 'e': 0.4164701851078906}, 'b': {'a': 0.2944888920518062, 'c': 0.4808983469629878, 'd': 0.4164701851078906, 'e': 0.4164701851078906}, 'd': {'a': 0.576853026474115, 'c': 0.470998523813926, 'b': 0.4164701851078906}, 'e': {'c': 0.4164701851078906, 'b': 0.4164701851078906}}

矩阵W的每列最大值

W_max: {'a': 0.576853026474115, 'c': 0.4808983469629878, 'b': 0.4808983469629878, 'd': 0.576853026474115, 'e': 0.4164701851078906}

物品类似度矩阵W(归一化以后)

a:{'b': 0.6123724356957947, 'd': 1.0}
c:{'b': 1.0, 'd': 0.8164965809277261, 'e': 1.0}
b:{'a': 0.5105093993383438, 'c': 1.0, 'd': 0.721969316263228, 'e': 1.0}
d:{'a': 1.0, 'c': 0.9794138964885573, 'b': 0.8660254037844387}
e:{'c': 0.8660254037844387, 'b': 0.8660254037844387}

用户A的推荐列表

c    1.9794138964885573
e    1.0

4、评估指标

TopN推荐: R ( u ) 是根据用户在训练集上的行为给用户作出的推荐列表, T ( u ) 是用户在测试集上的行为列表。
推荐结果的召回率:
召回率
推荐结果的准确率:
准确率
首先,将用户行为数据集按照均匀分布随机分红 M份(本文取 M =5 ),挑选一份做为测试集,将剩下的 M -1 份做为训练集。而后在训练集上创建用户兴趣模型,并在测试集上对用户行为进行预测,统计出相应的评测指标。为了保证评测指标并非过拟合的结果,须要进行 M 次实验,而且每次都使用不一样的测试集。而后将 M 次实验测出的评测指标的平均值做为最终的评测指标。
每次实验选取不一样的 k ( 0 ≤ k ≤ M - 1 )和相同的随机数种子 seed ,进行 M 次实验就能够获得 M 个不一样的训练集和测试集。
若是数据集够大,模型够简单,为了快速经过离线实验初步地选择算法,也能够只进行一次实验。

#-*-coding:utf-8 -*-
import math
import numpy as np
import random 
#进行5折交叉验证,计算平均准确率和召回率

class ItemBasedCF:  
    def __init__(self,data_file):  
        self.data_file = data_file 

    def splitData(self,k,M=5,seed=1):
        self.train_file = []
        self.test_file = []
        random.seed(seed)
        for line in open(self.data_file):
            if random.randint(0,M)==k:
                self.test_file.append(line)
            else:
                self.train_file.append(line)

    def readData(self,file):  
        #读取文件,并生成用户-物品的评分表 
        self.data_dict = dict()     #用户-物品的评分表 
        for line in file:
            tmp = line.strip().split(" ")
            if len(tmp)<3: continue
            user,score,item = tmp[:3]
            self.data_dict.setdefault(user,{})  
            self.data_dict[user][item] = int(float(score)) 
        return self.data_dict

    def ItemSimilarity(self):  
        #同上

    #给用户user推荐 
    def Recommend(self,user,K=10,N=100):  
        #同上

#声明一个ItemBased推荐的对象 
uid_score_bid='/home/lady/tmp/liushuang/1.9Item-basedCF/data/buy_user_spu.1210_0109'
Item = ItemBasedCF(uid_score_bid)#读取数据集 

M=5
pre_lst=[]
rec_lst=[]
for k in range(M): #进行5次交叉验证
    Item.splitData(k,M,seed=1)
    Item.train=Item.readData(Item.train_file)
    Item.test=Item.readData(Item.test_file) 

    Item.ItemSimilarity() #计算物品类似度矩阵 
    recommedDic = dict()
    hit = 0
    n_pre = 0
    n_rec = 0
    print '训练集数量',len(Item.train)
    print '测试集数量',len(Item.test)

    for user in  Item.train.keys():
        recommedDic[user] = Item.Recommend(user) #对于训练user生成推荐列表
        n_pre+=len(recommedDic[user])
        rec_item=set()
        for item in recommedDic[user]:
            rec_item.add(item[0])
        #测试user真实购买的商品
        buy_item=set()
        if user not in Item.test: continue
        for item in Item.test[user].keys():
            buy_item.add(item)
        hit += len(rec_item & buy_item)
    for user in Item.test:
        n_rec += len(Item.test[user])
    pre = hit/(1.0*n_pre)
    rec = hit/(1.0*n_rec)
    pre_lst.append(pre)
    rec_lst.append(rec)
    print k,' hit:',hit,'n_pre:',n_pre,'n_rec;',n_rec
print pre_lst,'平均:',np.mean(pre_lst)
print rec_lst,'平均:',np.mean(rec_lst)

参考:《推荐系统》基于用户和Item的协同过滤算法的分析与实现(Python)