在之前的博文中介绍了三种方法给用户推荐物品。
1)UserCF:给用户推荐和他们兴趣爱好相似的其他用户喜欢的物品。
2) ItemCF:给用户推荐与他喜欢过的物品相似的物品。
3) LFM:通过一些特征来联系用户和物品,给用户推荐那些具有用户喜欢的特征的物品。
具体可以看我之前的博文。
本文我将自己实现两个算法,如有不对的地方还望指正。
本节咱们将讨论一种重要的特征表现形式–标签。
标签:是一种无层次化结构的,用来描述信息的关键词,可以用来描述物品的语义。一般分为两种:
1):一种是让作者和专家给物品打标签。
2):一种是让普通用户给物品打标签,也就是UGC(User Generated Content)的标签应用。UGC的标签系统是一种表示用户兴趣和物品语义的重要方式。
本文将着重讨论UGC的标签应用。
标签系统中的推荐问题主要有以下两个:
1)如何利用用户打标签的行为为其推荐物品(基于标签的推荐)
2)如何在用户给物品打标签时为其推荐适合该物品的标签(标签推荐)
为了解决上面两个问题,我们首先需要解答下面3个问题
1)用户为什么打标签?
2)用户打什么样标签?
3)用户怎么打标签?
用户打标签的动机(从社会维度和功能角度分析)
社会角度:①便于上传者组织自己的信息) ②便于帮助其他用户找到信息
功能角度:①更好的标注内容,方便用户以后查找②传达某种信息,比如照片的拍摄时间和地点。
用户如何打标签
前面我们介绍了用户活跃度和物品流行度都符合长尾分布。(长尾分布的含义就是排名靠前的物品,虽然排名靠前,但是总的占比较小),因此我们有必要标签流行度的分布。我们定义一个标签被一个用户使用在一个物品上,则它的流行度加一。
#coding:utf-8
import pandas as pd
import matplotlib.pyplot as plt
'''
以delicious上2015-9数据进行实验
研究标签流行度的分布。
我们定义的一个标签被一个用户使用在一个物品上,它的流行度就加1
'''
data=pd.read_csv('200509',header=None,sep='\t')
data.columns=['date','user','website','label']
data.drop('date',axis=1,inplace=True)
def TagPopularity(records):
'''
tagfreq:字典,键值为标签,value为该标签被用户使用的次数,也即是该标签的流行度
:param records:data数据(用户,物品,标签)
:return:返回tagfreq
'''
tagfreq=dict()
for i in range(len(records)):
lst=list(records.iloc[i])
if lst[2] not in tagfreq:
tagfreq[lst[2]]=1
else:
tagfreq[lst[2]]+=1
return tagfreq
def get_k_nk(tagfreq):
'''
tag_pop:字典类型,键值是流行度k,value表示该流行度出现的次数
:param tagPop: TagPopularity方法返回的字典
:return: 返回tag_pop
'''
tag_pop=dict()
for tag,pop in tagfreq.items():
if pop not in tag_pop:
tag_pop[pop]=1
else:
tag_pop[pop]+=1
return tag_pop
if __name__=='__main__':
tagPop=TagPopularity(data)
tag_pop=get_k_nk(tagPop)
k = tag_pop.keys()#标签的流行度
nk = tag_pop.values()#标签流行度出现的次数
fig=plt.figure()
ax=fig.add_subplot(1,1,1)
ax.scatter(k, nk)
ax.set_xscale('log')#在分布极不均匀时对横坐标进行log缩放
ax.set_xticks([1,10,100,1000,10000,100000,1000000])#特别关注这几个区间的数值
plt.show()
横坐标标签流行度,纵坐标该流行度出现的次数。显然可以看出流行度越高的标签出现次数越少。说明占大多数的标签还是流行度不高的标签。符合长尾分布。
用户打什么样的标签
一般而言,用户打的标签能够反应物品内容属性的关键词。
基于标签的推荐系统
用户用标签来描述对物品的看法,因此标签是用户和物品的纽带,也是反应用户兴趣的重要数据。如何利用用户标签数据提高个性化推荐结果的质量是推荐系统研究的重要课题。
下面我们利用标签数据设计一个简单的算法(称为SimpleTagBase)来提高个性化推荐结果的质量。
算法描述:
1)统计每个用户最常用的标签
2)对于每个标签,统计被打过这个标签次数最多的物品
3)对于一个用户,首先找到他最常用的标签,然后找到具有这些标签的最热门物品推荐给这个用户。
基于上面的思路,我们可以定义用户u对物品i的兴趣度公式:
其中Nu,b表示用户u打过标签b的次数,Nb,i表示物品i被打过标签b的次数。依照上面的兴趣度公式可以计算出用户u对所有物品的兴趣,将物品兴趣从大到小排序即可以对用户u进行个性化TopN推荐。
实验思想:
取实验数据(user,item,tag),分为10份,9份作为训练集,1份作为测试集。这里分割的键值为(user,item)。也就是说用户对物品的多个标签要么全部分到训练集要么全部分到测试集。通过学习训练集中用户标签预测测试集上用户会给什么物品打标签。对于用户u,R(u)为给用户u的长度为N的推荐列表。T(u)为测试集中用户u实际上打过标签的全部的物品集合。然后用一系列评测标准来评价比较实验结果的好坏程度。这其中包括(召回率,准确率,覆盖率,多样性,流行度等),详细的介绍请看我以前的博文,这里介绍多样性中标签相似度是如何计算的:
计算标签相似度,如果认为同一个物品的不同的标签具有某种相似度,那么当两个标签同时出现在很多物品的集合,我们就可以认为这两个标签具有较大的相似度。
其中N(b)表示被打标签b的物品集合,N(b’)表示被打标签b’的物品集合。Nb,i表示物品i被打标签b的次数。
详细代码如下:
#coding:utf-8
import numpy as np
import pandas as pd
import random
import math
# 基于标签的推荐系统
# 以delicious上2015-9的数据作为实验数据
# 统计N(user,items):用户对items打标签的总次数
# 统计N(u,b):用户u打过标签b的次数
# 统计N(b,i):物品i被打标签b的次数
# P(u,i)=N(u,b)*N(b,i)表示用户u对物品i的感兴趣程度。
def genData():
'''
获取数据,取前10000行
:return:
'''
data=pd.read_csv('200509',header=None,sep='\t')
data.columns=['date','user','item','label']
data.drop('date',axis=1,inplace=True)
data=data[:50000]
print "genData successed!"
return data
def getUItem_label(data):
'''
UI_label:字典类型(user,item)->tag,tag是个列表,包含user对item打的多个标签,其中每个(user,item)是唯一的。
:param data:
:return: 返回UI_label
'''
UI_label=dict()
for i in range(len(data)):
lst=list(data.iloc[i])
user=lst[0]
item=lst[1]
label=lst[2]
addToMat(UI_label,(user,item),label)
print "UI_label successed!"
return UI_label
def addToMat(d,x,y):
d.setdefault(x,[ ]).append(y)
def SplitData(Data,M,k,seed):
'''
划分训练集和测试集
:param data:传入的数据
:param M:测试集占比
:param k:一个任意的数字,用来随机筛选测试集和训练集
:param seed:随机数种子,在seed一样的情况下,其产生的随机数不变
:return:train:训练集 test:测试集,都是字典,key是用户id,value是电影id集合
'''
data=Data.keys()
test=[]
train=[]
random.seed(seed)
# 在M次实验里面我们需要相同的随机数种子,这样生成的随机序列是相同的
for user,item in data:
if random.randint(0,M)==k:
# 相等的概率是1/M,所以M决定了测试集在所有数据中的比例
# 选用不同的k就会选定不同的训练集和测试集
for label in Data[(user,item)]:
test.append((user,item,label))
else:
for label in Data[(user, item)]:
train.append((user,item,label))
print "splitData successed!"
return train,test
def getTU(user,test,N):
'''
获取测试集中用户所打标签的物品集合
'''
items=set()
for user1,item,tag in test:
if user1!=user:
continue
if user1==user:
items.add(item)
return list(items)
def new_getTU(user,test,N):
'''
以测试集中用户对每个物品打标签次数按照从大到小排序,获取前N个物品集合。
'''
user_items=dict()
for user1,item,tag in test:
if user1!=user:
continue
if user1==user:
if (user,item) not in user_items:
user_items.setdefault((user,item),1)
else:
user_items[(user,item)]+=1
testN=sorted(user_items.items(), key=lambda x: x[1], reverse=True)[0:N]
items=[]
for i in range(len(testN)):
items.append(testN[i][0][1])
#if len(items)==0:print "TU is None"
return items
def Recall(train,test,user_items,user_tags,tag_items,N):
'''
:param train: 训练集
:param test: 测试集
:param N: TopN推荐中N数目
:param k:
:return:返回召回率
'''
hit=0# 预测准确的数目
totla=0# 所有行为总数
for user,item,tag in train:
tu=getTU(user,test,N)
rank=GetRecommendation(user,user_items,user_tags,tag_items,N)
for item in rank:
if item in tu:
hit+=1
totla+=len(tu)
print "Recall successed!",hit/(totla*1.0)
return hit/(totla*1.0)
def Precision(train,test,user_items,user_tags,tag_items,N):
'''
:param train:
:param test:
:param N:
:param k:
:return:准确率
'''
hit=0
total=0
for user, item, tag in train:
tu = getTU(user, test, N)
rank = GetRecommendation(user,user_items,user_tags,tag_items,N)
for item in rank:
if item in tu:
hit += 1
total += N
print "Precision successed!",hit / (total * 1.0)
return hit / (total * 1.0)
def Coverage(train,user_items,user_tags,tag_items,N):
'''
计算覆盖率
:param train:训练集 字典user->items
:param test: 测试机 字典 user->items
:param N: topN推荐中N
:param k:
:return:覆盖率
'''
recommend_items=set()
all_items=set()
for user, item, tag in train:
all_items.add(item)
rank=GetRecommendation(user,user_items,user_tags,tag_items,N)
for item in rank:
recommend_items.add(item)
print "Coverage successed!",len(recommend_items)/(len(all_items)*1.0)
return len(recommend_items)/(len(all_items)*1.0)
def Popularity(train,user_items,user_tags,tag_items,N):
'''
计算平均流行度
:param train:训练集 字典user->items
:param test: 测试机 字典 user->items
:param N: topN推荐中N
:param k:
:return:覆盖率
'''
item_popularity=dict()
for user, item, tag in train:
if item not in item_popularity:
item_popularity[item]=0
item_popularity[item]+=1
ret=0
n=0
for user, item, tag in train:
rank= GetRecommendation(user,user_items,user_tags,tag_items,N)
for item in rank:
if item!=0 and item in item_popularity:
ret+=math.log(1+item_popularity[item])
n+=1
if n==0:return 0.0
ret/=n*1.0
print "Popularity successed!",ret
return ret
def CosineSim(item_tags,item_i,item_j):
'''
两个不同物品的相似程度
:param item_tags: 字典(item->tags->count)物品item被打标签tag的次数count
:param item_i:
:param item_j:
:return:
'''
ret=0
for b,wib in item_tags[item_i].items():
if b in item_tags[item_j]:
ret+=wib*item_tags[item_j][b]
ni=0
nj=0
for b,w in item_tags[item_i].items():
ni+=w*w
for b,w in item_tags[item_j].items():
nj+=w*w
if ret==0:
return 0
return ret/math.sqrt(ni*nj)
def Diversity(train,user_items,user_tags,tag_items,N,item_tags):
'''
:param train:
:param user_items:
:param user_tags:
:param tag_items:
:param N:
:param item_tags:
:return: 推荐列表的多样性
'''
ret=0.0
n=0
for user, item, tag in train:
rank = GetRecommendation(user,user_items,user_tags,tag_items,N)
for item1 in rank:
for item2 in rank:
if item1==item2:
continue
else:
ret+=CosineSim(item_tags,item1,item2)
n+=1
print n,ret
print "Diversity successed!",ret /(n*1.0)
return ret /(n*1.0)
def InitStat(record):
user_tags=dict()
tag_items=dict()
user_items=dict()
item_tags=dict()
for user,item,tag in record:
if user not in user_tags:
user_tags[user]=dict()
if tag not in user_tags[user]:
user_tags[user][tag]=1
else:
user_tags[user][tag]+=1
if tag not in tag_items:
tag_items[tag]=dict()
if item not in tag_items[tag]:
tag_items[tag][item]=1
else:
tag_items[tag][item]+=1
if user not in user_items:
user_items[user]=dict()
if item not in user_items[user]:
user_items[user][item]=1
else:
user_items[user][item]+=1
if item not in item_tags:
item_tags[item]=dict()
if tag not in item_tags[item]:
item_tags[item][tag]=1
else:
item_tags[item][tag]+=1
return user_items,user_tags,tag_items,item_tags
def GetRecommendation(user,user_items,user_tags,tag_items,N):
'''
:param user:
:param user_items:
:param user_tags:
:param tag_items:
:param N:
:return: 返回推荐列表中TopN个物品集合。
'''
recommend_items=dict()
for tag,wut in user_tags[user].items():
for item,wti in tag_items[tag].items():
if item in user_items[user]:
continue
elif item not in recommend_items:
recommend_items[item]=wut*wti
else:
recommend_items[item]+=wut*wti
itemN = dict(sorted(recommend_items.items(), key=lambda x: x[1], reverse=True)[:N])
return itemN.keys()
def evaluate(train,test,N,user_items, user_tags, tag_items,item_tags):
##计算一系列评测标准
recommends=dict()
# for user, item, tag in test:
# recommends[user]=GetRecommendation(user,train,user_items,user_tags,tag_items,N)
recall=Recall(train,test,user_items,user_tags,tag_items,N)
precision=Precision(train,test,user_items,user_tags,tag_items,N)
coverage=Coverage(train,user_items,user_tags,tag_items,N)
popularity=Popularity(train,user_items,user_tags,tag_items,N)
diversity=Diversity(train,user_items,user_tags,tag_items,N,item_tags)
return recall,precision,coverage,popularity,diversity
if __name__=='__main__':
data=genData()
UI_label = getUItem_label(data)
(train, test) = SplitData(UI_label, 10, 5, 10)
N=20
user_items, user_tags, tag_items, item_tags = InitStat(train)
recall, precision, coverage, popularity, diversity = evaluate(train, test, N, user_items, user_tags, tag_items,item_tags)
实验结果
(‘Recall: ‘, 0.027900836801360362)
(‘Precision: ‘, 0.0013721088884487576)
(‘Coverage: ‘, 0.5118665308999765)
(‘Popularity: ‘, 3.6241000994222556)
(‘Diversity: ‘, 0.19262789691530108)
算法改进(TagBaseTFIDF):上面的简单算法是通过以下公式预测用户u对物品i的兴趣程度
显然这样预测有个明显的缺点,这个公式倾向于给热门标签对应的热门物品很大的权重,因此会造成推荐热门的物品给用户,从而降低推荐结果的新颖度。我们可以借鉴IFIDF思想,对这一公式进行改进,对热门物品的权重进行惩罚:
这里记录了标签b被多少个不同的用户使用过。
该算法代码如下:
#coding:utf-8
import numpy as np
import pandas as pd
import random
import math
'''
基于标签的推荐系统
以delicious上2015-9的数据作为实验数据
统计N(user,items):用户对items打标签的总次数
统计N(u,b):用户u打过标签b的次数
统计N(b,i):物品i被打标签b的次数
统计Nbu:标签b被多少个不同的用户使用过。
P(u,i)=N(u,b)*N(b,i)/log(1+Nbu)表示用户u对物品i的感兴趣程度。
'''
def genData():
data=pd.read_csv('200509',header=None,sep='\t')
data.columns=['date','user','item','label']
data.drop('date',axis=1,inplace=True)
data=data[:50000]
print "genData sucessed!"
return data
def getUItem_label(data):
UI_label=dict()
for i in range(len(data)):
lst=list(data.iloc[i])
user=lst[0]
item=lst[1]
label=lst[2]
addToMat(UI_label,(user,item),label)
print "UI_label successed!"
return UI_label
def addToMat(d,x,y):
d.setdefault(x,[ ]).append(y)
def SplitData(Data,M,k,seed):
'''
划分训练集和测试集
:param data:传入的数据
:param M:测试集占比
:param k:一个任意的数字,用来随机筛选测试集和训练集
:param seed:随机数种子,在seed一样的情况下,其产生的随机数不变
:return:train:训练集 test:测试集,都是字典,key是用户id,value是电影id集合
'''
data=Data.keys()
test=[]
train=[]
random.seed(seed)
# 在M次实验里面我们需要相同的随机数种子,这样生成的随机序列是相同的
for user,item in data:
if random.randint(0,M)!=k:
# 相等的概率是1/M,所以M决定了测试集在所有数据中的比例
# 选用不同的k就会选定不同的训练集和测试集
for label in Data[(user,item)]:
test.append((user,item,label))
else:
for label in Data[(user, item)]:
train.append((user,item,label))
print "splitData sucessed!"
return train,test
def getTU(user,test,N):
'''
获取测试集中用户所打标签的物品集合
'''
items=set()
for user1,item,tag in test:
if user1!=user:
continue
if user1==user:
items.add(item)
return list(items)
def new_getTU(user,test,N):
'''
以测试集中用户对每个物品打标签次数按照从大到小排序,获取前N个物品集合。
'''
user_items=dict()
for user1,item,tag in test:
if user1!=user:
continue
if user1==user:
if (user,item) not in user_items:
user_items.setdefault((user,item),1)
else:
user_items[(user,item)]+=1
testN = sorted(user_items.items(), key=lambda x: x[1], reverse=True)[0:N]
items = []
for i in range(len(testN)):
items.append(testN[i][0][1])
return items
def Recall(train,test,user_items,user_tags,tag_items,N,tag_users):
'''
:param train: 训练集
:param test: 测试集
:param N: TopN推荐中N数目
:param k:
:return:返回召回率
'''
hit=0# 预测准确的数目
totla=0# 所有行为总数
for user,item,tag in train:
tu=getTU(user,test,N)
rank=GetRecommendation(user,user_items,user_tags,tag_items,N,tag_users)
for item in rank:
if item in tu:
hit+=1
totla+=len(tu)
print "Recall sucessed! ",hit/(totla*1.0)
return hit/(totla*1.0)
def Precision(train,test,user_items,user_tags,tag_items,N,tag_users):
'''
:param train:
:param test:
:param N:
:param k:
:return:
'''
hit=0
total=0
for user, item, tag in train:
tu = getTU(user, test, N)
rank = GetRecommendation(user,user_items,user_tags,tag_items,N,tag_users)
for item in rank:
if item in tu:
hit += 1
total += N
print "Precision successed! ",hit / (total * 1.0)
return hit / (total * 1.0)
def Coverage(train,user_items,user_tags,tag_items,N,tag_users):
'''
计算覆盖率
:param train:训练集 字典user->items
:param test: 测试机 字典 user->items
:param N: topN推荐中N
:param k:
:return:覆盖率
'''
recommend_items=set()
all_items=set()
for user, item, tag in train:
all_items.add(item)
rank=GetRecommendation(user,user_items,user_tags,tag_items,N,tag_users)
for item in rank:
recommend_items.add(item)
print "Coverage successed!",len(recommend_items)/(len(all_items)*1.0)
return len(recommend_items)/(len(all_items)*1.0)
def Popularity(train,user_items,user_tags,tag_items,N,tag_users):
'''
计算平均流行度
:param train:训练集 字典user->items
:param test: 测试机 字典 user->items
:param N: topN推荐中N
:param k:
:return:流行度
'''
item_popularity=dict()
for user, item, tag in train:
if item not in item_popularity:
item_popularity[item]=0
item_popularity[item]+=1
ret=0
n=0
for user, item, tag in train:
rank= GetRecommendation(user,user_items,user_tags,tag_items,N,tag_users)
for item in rank:
if item!=0 and item in item_popularity:
ret+=math.log(1+item_popularity[item])
n+=1
if n==0:return 0
ret/=n*1.0
print "Popularity successed!",ret
return ret
def CosineSim(item_tags,item_i,item_j):
ret=0
for b,wib in item_tags[item_i].items():
if b in item_tags[item_j]:
ret+=wib*item_tags[item_j][b]
ni=0
nj=0
for b,w in item_tags[item_i].items():
ni+=w*w
for b,w in item_tags[item_j].items():
nj+=w*w
if ret==0:
return 0
return ret/math.sqrt(ni*nj)
def Diversity(train,user_items,user_tags,tag_items,N,item_tags,tag_users):
ret=0
n=0
for user, item, tag in train:
rank = GetRecommendation(user,user_items,user_tags,tag_items,N,tag_users)
for item1 in rank:
for item2 in rank:
if item1==item2:
continue
ret+=CosineSim(item_tags,item1,item2)
n+=1
if n==0:return 0.0
print "Diversity successed! ",ret /(n*1.0)
return ret /(n*1.0)
def InitStat(record):
user_tags=dict()
tag_items=dict()
user_items=dict()
item_tags=dict()
tag_users=dict()
for user,item,tag in record:
if tag not in tag_users:
tag_users[tag]=dict()
if user not in tag_users[tag]:
tag_users[tag][user]=1
else:
tag_users[tag][user]+=1
if user not in user_tags:
user_tags[user]=dict()
if tag not in user_tags[user]:
user_tags[user][tag]=1
else:
user_tags[user][tag]+=1
if tag not in tag_items:
tag_items[tag]=dict()
if item not in tag_items[tag]:
tag_items[tag][item]=1
else:
tag_items[tag][item]+=1
if user not in user_items:
user_items[user]=dict()
if item not in user_items[user]:
user_items[user][item]=1
else:
user_items[user][item]+=1
if item not in item_tags:
item_tags[item]=dict()
if tag not in item_tags[item]:
item_tags[item][tag]=1
else:
item_tags[item][tag]+=1
print "initState successed!"
return user_items,user_tags,tag_items,item_tags,tag_users
def getNbu(tag,tag_users):
'''
:param tag:标签b
:param tag_users:字典类型(tag->user->count)标签tag被用户user打的次数count
:return:
'''
nbu=0
for user,wut in tag_users[tag].items():
nbu+=1#因为是被不同用户打的次数,故这里+1,不是+wut
return nbu
def GetRecommendation(user,user_items,user_tags,tag_items,N,tag_users):
recommend_items=dict()
nbu=0
for tag,wut in user_tags[user].items():
nbu+=np.log(1+getNbu(tag,tag_users))
for item,wti in tag_items[tag].items():
if item in user_items[user]:
continue
elif item not in recommend_items:
recommend_items[item]=wut*wti/nbu
else:
recommend_items[item]+=wut*wti/nbu
itemN = dict(sorted(recommend_items.items(), key=lambda x: x[1], reverse=True)[:N])
return itemN.keys()
def evaluate(train,test,N,user_items, user_tags, tag_items,item_tags,tag_users):
##计算一系列评测标准
recall=Recall(train,test,user_items,user_tags,tag_items,N,tag_users)
precision=Precision(train,test,user_items,user_tags,tag_items,N,tag_users)
coverage=Coverage(train,user_items,user_tags,tag_items,N,tag_users)
popularity=Popularity(train,user_items,user_tags,tag_items,N,tag_users)
diversity=Diversity(train,user_items,user_tags,tag_items,N,item_tags,tag_users)
return recall,precision,coverage,popularity,diversity
if __name__=='__main__':
data=genData()
UI_label = getUItem_label(data)
(train, test) = SplitData(UI_label, 10, 5, 10)
N=20
user_items, user_tags, tag_items, item_tags,tag_users= InitStat(train)
recall, precision, coverage, popularity, diversity = evaluate(train, test, N, user_items, user_tags, tag_items,item_tags,tag_users)
print("Recall: ", recall)
print("Precision: ", precision)
print("Coverage: ", coverage)
print("Popularity: ", popularity)
print("Diversity: ", diversity)
实验结果
(‘Recall: ‘, 0.010166568329324195)
(‘Precision: ‘, 0.004913358192586093)
(‘Coverage: ‘, 0.7181208053691275)
(‘Popularity: ‘, 1.8854622713175704)
(‘Diversity: ‘, 0.2273525361158989)
TagBaseTFIDF算法对比SimpleTagBase算法,在某些指标上稍有下降,在某些指标上上升较大。这里我只取了前5万行数据集,增大数据集,效果更加明显。当然以上算法还可以改进对热门物品的惩罚这里就不再讨论了。
数据稀疏性
对于新物品和新用户,对应的标签集合的标签数量可能很少,为了提高推荐准确率,我们需要对标签做扩展,比如某个新用户打过的标签很少,我们可以在标签集合里加入类似于打过的标签的其他标签。标签的相似度计算上面已经详细说明。基于图的推荐算法看我另一篇博文详细介绍。
基于标签的推荐系统中利用图的推荐算法(PersonalRank)