基于物品的协同过滤算法(简称ItemCF)给用户推荐那些和他们之前喜欢的物品相似的物品。不过ItemCF不是利用物品的内容计算物品之间相似度,而是利用用户的行为记录。
该算法认为,物品A和物品B具有很大的相似度是因为喜欢物品A的用户大都也喜欢物品B。这里蕴含一个假设,就是每个用户的兴趣都局限在某几个方面,因此如果两个物品属于同一个用户的兴趣列表,那么这两个物品可能就属于有限的几个领域。而如果两个物品同时出现在很多用户的兴趣列表,那么它们可能就属于同一领域,因而具有很大的相似度。
从上述概念出发,定义物品i和j的相似度为
其中, |N(i)| | N ( i ) | 是喜欢物品i的用户数, |N(i)⋂N(j)| | N ( i ) ⋂ N ( j ) | 是同时喜欢物品i和物品j的用户数。分母是惩罚物品i和j的权重,因此惩罚了热门物品和很多物品相似的可能性。
在得到物品相似度之后,ItemCF通过以下公式计算用户u对未产生行为的物品j的感兴趣程度。
这里的 N(u) N ( u ) 是用户喜欢的物品集合, S(j,K) S ( j , K ) 是和物品j最相似的K个物品的集合, wij w i j 是物品j和i的相似度, rui r u i 是用户u对物品j的兴趣评分(简单的,如果用户u对物品i有过行为,即可令 rui r u i =1)
下面举一个例子说明(只考虑用户有历史购买行为的物品)。
用户A购买物品a b d,用户B购买物品b c e,用户C购买物品c d,用户D购买物品b c d,用户E购买物品a d。
user | item |
---|---|
A | a b d |
B | b c e |
C | c d |
D | b c d |
E | a d |
数据集格式为(用户, rui r u i =1,物品),每行记录都是唯一的,兴趣评分由 rui r u i 决定。
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.train
{'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}}
物品被多少个不同用户购买
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}}
物品相似矩阵
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
在ItemCF中,两个物品产生相似度是因为它们共同出现在很多用户的兴趣列表中。假设有这么一个用户,他是开书店的,并且买了当当网上 80% 的书准备用来自己卖。那么,
他的购物车里包含当当网 80% 的书。所以这个用户对于他所购买书的两两相似度的贡献应该远远小于一个只买了十几本自己喜欢的书的文学青年。
提出一个称为 IUF ( Inverse User Frequence ),即用户活跃度对数的倒数的参数,来修正物品相似度的计算公式。认为活跃用户对物品相似度的贡献应该小于不活跃的用户。
对于已经得到的物品相似度矩阵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
TopN推荐: R(u) R ( u ) 是根据用户在训练集上的行为给用户做出的推荐列表, T(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)