1:协同过滤算法简介
2:协同过滤算法的核心
3:协同过滤算法的应用方式
4:基于用户的协同过滤算法实现
5:UserCF中相似度计算部分优化
一:协同过滤算法简介
基于用户的协同过滤算法是推荐系统中最古老的的算法,可以说是这个算法的诞生标志了推荐系统的诞生。该算法在1992年被提出,并应用于邮件过滤系统,1994年被GroupLens用于新闻过滤。
在一个在线个性化推荐系统中,当一个用户A需要个性化推荐时,可以先找到和他有相似兴趣的其他用户,然后把那些用户喜欢的而用户A没有接触过的物品推荐给A。这种方法称为基于用户的协同过滤算法。
关于协同过滤的一个最经典的例子就是看电影,有时候不知道哪一部电影是我们喜欢的或者评分比较高的,那么通常的做法就是问问周围的朋友,看看最近有什么好的电影推荐。在问的时候,都习惯于问跟自己口味差不 多的朋友,这就是协同过滤的核心思想。
协同过滤是在海量数据中挖掘出小部分与你品味类似的用户,在协同过滤中,这些用户成为邻居,然后根据他们喜欢的东西组织成一个排序的目录推荐给你。所以就有如下两个核心问题
(1)如何确定一个用户是否与你有相似的品味?
(2)如何将邻居们的喜好组织成一个排序目录?
协同过滤算法的出现标志着推荐系统的产生,协同过滤算法包括基于用户和基于物品的协同过滤算法。
二:协同过滤算法的核心
要实现协同过滤,需要进行如下几个步骤
1)收集用户偏好
2)找到相似的用户或者物品
3)计算并推荐
三:协同过滤算法的应用方式
1:基于用户的协同过滤算法
基于用户的协同过滤通过不同用户对物品的评分来评测用户之间的相似性,基于用户的相似性做推荐,简单的讲:给用户推荐和他兴趣相投的其他用户喜欢的物品
基于用户的协同过滤算法主要包括两个步骤。
(1) 找到和目标用户兴趣相似的用户集合。
(2) 找到这个集合中的用户喜欢的,且目标用户没有听说过的物品推荐给目标用户。
算法实现流程分析:
步骤(1)的关键就是计算两个用户的兴趣相似度。这里,协同过滤算法主要利用行为的相似度计算兴趣的相似度。给定用户u和用户v,令N(u)表示用户u曾经有过正反馈的物品集合,令N(v)为用户v曾经有过正反馈的物品集合。那么,我们可以通过如下的Jaccard公式简单地计算u和v的兴趣相似度或者通过余弦公式。
Jaccard系数:两个集合A和B交集元素的个数在A、B并集中所占的比例,称为这两个集合的杰卡德系数,用符号J(A,B) 表示。杰卡德相似系数是衡量两个集合相似度的一种指标(余弦距离也可以用来衡量两个集合的相似度)。
Jaccard(基于共同邻居的一个显著问题是用户的物品集越大,越可能与其他用户相似。Jaccard系数消除了这一影响)公式:
余弦公式:
下图是一个用户行为记录,我们可以根据余弦公式计算如下:
计算用户的相似度,例如A,B为
同理
这种方法的时间复杂度是O(|U|*|U|),这在用户数很大时非常耗时。事实上,很多用户相互之间并没有对同样的物品产生过行为,即很多时候N(u)^ N(v) = 0。上面的算法将很多时间浪费在了计算这种用户之间的相似度上。如果换一个思路,我们可以首先计算出N(u)^ N(v) != 0 的用户对(u,v),然后再对这种情况除以分母sqrt(N(u)*N(v)) 。
为此,可以首先建立物品到用户的倒排表,对于每个物品都保存对该物品产生过行为的用户列表。令稀疏矩阵C[u][v]= N(u)^ N(v) 。那么,假设用户u和用户v同时属于倒排表中K个物品对应的用户列表,就有C[u][v]=K。从而,可以扫描倒排表中每个物品对应的用户列表,将用户列表中的两两用户对应的C[u][v]加1,最终就可以得到所有用户之间不为0的C[u][v]。
下面是按照想法建立的稀疏矩阵,对于物品a,将W[A][B]和W[B][A]加1,对于物品b,将W[A][C]和W[C][A]加1,以此类推,扫描完所有物品后,我们可以得到最终的W矩阵,这里的W是余弦相似度中的分子部分,然后将W除以分母可以得到最终的用户兴趣相似度
得到用户的相似度后,便可以进行下一步了
(2):给用户推荐兴趣最相近的k个用户所喜欢的物品
公式如下:
其中,p(u,i)表示用户u对物品i的感兴趣程度,S(u,k)表示和用户u兴趣最接近的K个用户,N(i)表示对物品i有过行为的用户集合,Wuv表示用户u和用户v的兴趣相似度,Rvi表示用户v对物品i的兴趣(这里简化,所有的Rvi都等于1)。
根据UserCF算法,可以算出,用户A对物品c、e的兴趣是:
下面采用数据集进行实验,数据集选取了MovieLens 10M的数据集
链接:http://grouplens.org/datasets/movielens/
这个数据集是很多用户对各种电影的评分。里面包含movies.dat等文件,用text打开后格式如下:
而我们需要以表格形式读取数据,因此需要用到python的Texttable第三方包。可以直接在cmd中使用命令行 pip install texttable下载,也可以去官网下载相应的包。
大致用法如下
from numpy import *
import time
from texttable import Texttable
class CF:
def __init__(self, movies, ratings, k=5, n=10):
self.movies = movies
self.ratings = ratings
# 邻居个数
self.k = k
# 推荐个数
self.n = n
# 用户对电影的评分
# 数据格式{'UserID:用户ID':[(MovieID:电影ID,Rating:用户对电影的评星)]}
self.userDict = {}
# 对某电影评分的用户
# 数据格式:{'MovieID:电影ID',[UserID:用户ID]}
# {'1',[1,2,3..],...}
self.ItemUser = {}
# 邻居的信息
self.neighbors = []
# 推荐列表
self.recommandList = []
self.cost = 0.0
# 基于用户的推荐
# 根据对电影的评分计算用户之间的相似度
def recommendByUser(self, userId):
self.formatRate()
# 推荐个数 等于 本身评分电影个数,用户计算准确率
self.n = len(self.userDict[userId])
self.getNearestNeighbor(userId)
self.getrecommandList(userId)
self.getPrecision(userId)
# 第五步:获取推荐列表
def getrecommandList(self, userId):
# recommandList = [[neighbor, movieID]]
self.recommandList = []
# 建立推荐字典
# self.neighbors = [[dist, i(表示neighbors)]]
# recommandDict = {movieID:[neighbor]}
recommandDict = {}
for neighbor in self.neighbors:
#movies = [movieID,Rating]
movies = self.userDict[neighbor[1]]
for movie in movies:
if (movie[0] in recommandDict):
recommandDict[movie[0]] += neighbor[0]
else:
recommandDict[movie[0]] = neighbor[0]
# 建立推荐列表
for key in recommandDict:
self.recommandList.append([recommandDict[key], key])
self.recommandList.sort(reverse=True)
# 取出降序后列表前n个推荐电影数的列表
self.recommandList = self.recommandList[:self.n]
# 第一步:将ratings转换为userDict和ItemUser
def formatRate(self):
# userDict 用户对电影的评分
# 数据格式{'UserID:用户ID':[(MovieID:电影ID,Rating:用户对电影的评星)]}
self.userDict = {}
# ItemUser 对某电影评分的用户
# 数据格式:{'MovieID:电影ID',[UserID:用户ID]}
# {'1',[1,2,3..],...}
self.ItemUser = {}
for i in self.ratings:
# 评分最高为5 除以5 进行数据归一化
temp = (i[1], float(i[2]) / 5)
# 计算userDict {'1':[(1,5),(2,5)...],'2':[...]...}
if (i[0] in self.userDict):
self.userDict[i[0]].append(temp)
else:
self.userDict[i[0]] = [temp]
# 计算ItemUser {'1',[1,2,3..],...}
if (i[1] in self.ItemUser):
self.ItemUser[i[1]].append(i[0])
else:
self.ItemUser[i[1]] = [i[0]]
# 第二步:找到某用户的相邻用户
def getNearestNeighbor(self, userId):
neighbors = []
#self.neighbors = [[dist, i]]
self.neighbors = []
# 获取userId评分的电影都有那些用户也评过分
for i in self.userDict[userId]:
for j in self.ItemUser[i[0]]:
if (j != userId and j not in neighbors):
neighbors.append(j)
# 计算这些用户与userId的相似度并排序
for i in neighbors:
dist = self.getCost(userId, i)
self.neighbors.append([dist, i])
# 排序默认是升序,reverse=True表示降序
self.neighbors.sort(reverse=True)
#取出前k个neighbors
self.neighbors = self.neighbors[:self.k]
# 第四步:格式化userDict数据
def formatuserDict(self, userId, l):
#user有3种格式,{movieID:[Ratings(userID),0]},{movieID:[0,Ratings(l)]},{movieID:[Ratings(userID),Ratings(l)]}
user = {}
for i in self.userDict[userId]:
user[i[0]] = [i[1], 0]
for j in self.userDict[l]:
#如果用户l的movieID没有在用户userID的字典中时,就以{movieID:[0,Ratings(l)]}的形式加入user字典
if (j[0] not in user):
user[j[0]] = [0, j[1]]
#否则,就将用户l对应的电影评分替换掉user值中的0,若替换,则是{movieID:[Ratings(userID),Ratings(l)]}的格式
else:
user[j[0]][1] = j[1]
return user
# 第三步:计算余弦距离
def getCost(self, userId, l):
# 获取用户userId和l评分电影的并集
# {'电影ID':[userId的评分,l的评分]} 没有评分为0
user = self.formatuserDict(userId, l)
x = 0.0
y = 0.0
z = 0.0
for k, v in user.items():
x += float(v[0]) * float(v[0])
y += float(v[1]) * float(v[1])
z += float(v[0]) * float(v[1])
if (z == 0.0):
return 0
return z / sqrt(x * y)
# 第六步:推荐的准确率
# userDict={'UserID':[(MovieID,Rating)]}
# recommandList = [[neighbor, movieID]]
def getPrecision(self, userId):
user = [i[0] for i in self.userDict[userId]]
recommand = [i[1] for i in self.recommandList]
count = 0.0
if (len(user) >= len(recommand)):
for i in recommand:
if (i in user):
count += 1.0
self.cost = count / len(recommand)
else:
for i in user:
if (i in recommand):
count += 1.0
self.cost = count / len(user)
# 显示推荐列表
def showTable(self):
neighbors_id = [i[1] for i in self.neighbors]
table = Texttable()
table.set_deco(Texttable.HEADER)
table.set_cols_dtype(["t", "t", "t", "t"])
table.set_cols_align(["l", "l", "l", "l"])
rows = []
rows.append([u"movie ID", u"Name", u"release", u"from userID"])
# recommandList = [[neighbor, movieID]]
# movies = [movieID,Rating]
# ItemUser={'MovieID', [UserID]}
for item in self.recommandList:
fromID = []
for i in self.movies:
if i[0] == item[1]:
movie = i
break
for i in self.ItemUser[item[1]]:
if i in neighbors_id:
fromID.append(i)
movie.append(fromID)
rows.append(movie)
table.add_rows(rows)
print(table.draw())
# 获取数据
def readFile(filename):
files = open(filename, "r", encoding="utf-8")
# 如果读取不成功试一下
# files = open(filename, "r", encoding="iso-8859-15")
data = []
for line in files.readlines():
item = line.strip().split("::")
data.append(item)
return data
# -------------------------开始-------------------------------
start = time.clock()
movies = readFile("C:/Practice Data/UserCF_movies/movies.dat")
ratings = readFile("C:/Practice Data/UserCF_movies/ratings.dat")
demo = CF(movies, ratings, k=20)
demo.recommendByUser("100")
print("推荐列表为:")
demo.showTable()
print("处理的数据为%d条" % (len(demo.ratings)))
print("准确率: %.2f %%" % (demo.cost * 100))
end = time.clock()
print("耗费时间: %f s" % (end - start))
运行了大概70分钟,结果如下:
五:UserCF中相似度计算部分优化
首先建立物品到用户的倒排表,对于每个物品都保存对该物品产生过行为的用户列表。令稀疏矩阵C[u][v]= N(u)^ N(v) 。那么,假设用户u和用户v同时属于倒排表中K个物品对应的用户列表,就有C[u][v]=K。从而,可以扫描倒排表中每个物品对应的用户列表,将用户列表中的两两用户对应的C[u][v]加1,最终就可以得到所有用户之间不为0的C[u][v]。
下面是按照想法建立的稀疏矩阵,对于物品a,将W[A][B]和W[B][A]加1,对于物品b,将W[A][C]和W[C][A]加1,以此类推,扫描完所有物品后,我们可以得到最终的W矩阵,这里的W是余弦相似度中的分子部分,然后将W除以分母可以得到最终的用户兴趣相似度
这里采用python中的字典存储矩阵信息。
代码如下:
Item_User ={'a':[1,2,3],'b':[1,3,4],'c':[2,3,4],'d':[1,2],'e':[1,5]}
list1 = []
for k, v in Item_User.items():
for i in v:
if i not in list1:
list1.append(i)
print(list1)
# 存储item[u][v]的计数
dict = {}
k = 0
for t, users in Item_User.items():
k += 1
for u in list1:
for v in list1:
tuple1 = (u, v)
if k==1:
if u==v:
count_u_v = 0
dict[tuple1] = count_u_v
elif u in users and v in users:
count_u_v = 1
dict[tuple1] = count_u_v
else:
count_u_v = 0
dict[tuple1] = count_u_v
else:
if u==v:
count_u_v = 0
elif u in users and v in users:
count_u_v = dict[tuple1] + 1
else:
count_u_v = dict[tuple1]
dict[tuple1] = count_u_v
print(dict)
结果如下:
字典的key为稀疏矩阵C[u][v],value为C[u][v]的值。
借助上面的小测试,修改原先的UserCF代码,未完待续。