搭建你的第一个推荐系统
初识推荐系统
最初的推荐系统,作用是过滤垃圾邮件。今日机器学习算法的发展,朴素贝叶斯、神经网络已然成为了过滤垃圾邮件的好手。但是在30年前,算法还没有如今使用得广泛的时候,“找相同”就成为了过滤垃圾邮件的唯一选择。
简单的说,假设一个用户A和用户B都对许多相同邮件打上了“×”的标签,则当用户A对某一封邮件打上“×”的标签的时候,我们有理由相信,用户B也有很大可能会对这封邮件打上“×”的标签。
这便是基于用户行为来进行推荐服务,业界将这称为协同过滤算法。如今应用的最广泛的两种协同过滤算法分别是:
- 基于用户行为的协同过滤算法:找出相似用户,假设A和B这两个用户相似,则用户A喜欢的物品就有理由推荐给用户B。
- 基于物品属性的协同过滤算法:找出相似物品,假设A和B这两个物品相似,则用户对物品A产生了行为则很有可能也会对物品B产生行为。
相似性度量
那么首要的问题便是,相似性怎么度量?我们有以下选择方案
- 余弦相似度
- 皮尔逊相关系数
- …
余弦相似度
思考一下,假设用户P1、P2对物品A、物品B、物品C都产生了行为,而用户P3对物品B、物品C、物品D产生了行为,试问P1和P2相似度更高还是和P3相似度更高?
我们最初可能会写一段代码:
# 假设现在我们有几个用户的数据,我想利用某种方法来计算他们之间的相关系数
# 表示用户person1对A物品产生行为,且他对A的喜欢度为5,其他以此类推
person1 = {"A": 5, "B": 4, "C": 3}
person2 = {"A": 2, "B": 5, "D": 4}
person3 = {"B": 3, "C": 4, "E": 5}
person4 = {"A": 1, "C": 5, "D": 4}
person5 = {"A": 2, "B": 5, "C": 4}
person6 = {"A": 2, "B": 5, "C": 5}
person7 = {"A": 5, "B": 4, "C": 2}
# 简单的计算相似度的方法
def getCorr1(p1, p2):
score = 0 # 得分
# 遍历用户产生行为的物品
for (item, rate) in p1.items():
if item in p2.keys():
score += 1 # 简单的,只要有相同,我们就加1
return score
这个便是余弦相似度的前身,懂了上述代码之后,就可以很好的理解余弦相似度。
这里的u和v代表两个不同的用户,N则代表他们产生行为的物品
def getCorr2(p1, p2):
num_intersection = len(set(p1) & set(p2)) # 求交集,先要转换成字典
num_union_set = math.sqrt(len(p1) * len(p2) * 1.0) # 求并集,最后开方
return num_intersection / num_union_set # 返回除的结果
细心的朋友可能会发现,这样计算,person1和person5计算的结果,person1和person7计算的结果是一样的啊!
那或许我们可以试试看皮尔逊相关系数
皮尔逊相关系数
皮尔逊相关系数是使用协方差除以两个变量的标准差得到的,当两个变量的方差都不为0时,相关系数才有意义。
这里使用《推荐系统开发实战》中Netflix数据集的例子。数据集中的training_set.tar是官方训练集,而我们要做的是,将官方训练集划分成训练集和测试集,以测试算法的效果。
Netflix数据集是电影数据集,包含了很多电影的相关信息。而training_set.tar,解压之后的文件夹中包含着每部电影为后缀命名的文件,第一行为电影ID,后边的每行为用户评分行为。每列分别代表用户ID,评分值,时间。
我们用一个类来搭建推荐系统,这个类包含初始化函数、加载数据函数、随机选取用户函数、计算用户相似度函数、电影推荐函数、评估函数。
编写初始化函数
# 初始化函数
def __init__(self, file_path, seed, k, n_items):
self.file_path = file_path # 获取文件路径
self.users = self.select_users() # 获取随机获得的1000个用户
self.seed = seed # 获取随机数种子
self.k = k # 选取的近邻用户个数
self.n_items = n_items # 为每个用户推荐的电影数目
self.train, self.test = self.load_and_split_data() # 获取训练集和测试集
编写随机选取用户函数:因为数据量巨大,所以随机选取1000个用户来进行开发。就算是这样,耗时也得十几分钟…
def select_users(self):
"""
获取所有用户,并随机选择1000个,然后返回
:return: 返回值是列表形式的用户数据
"""
# 加载的数据会以json格式保存下来,之后使用直接加载json格式的数据
if os.path.exists("data/train.json") and os.path.exists("data/test.json"):
print("数据存在,从文件中加载数据!")
return list()
else:
print("数据不存在,随机获取1000个用户!")
users = set() # 用集合来存储所有用户
# os.listdir返回一个列表,现在要做的是将该目录下所有的文件都加载进来
for file in os.listdir(self.file_path):
# 拼接成每一个文件的路径
one_path = "{}/{}".format(self.file_path, file)
# 以"只读"的形式打开文件
with open(one_path, "r") as fp:
# 读取所有行,fp.readlines()返回的是一个列表
for line in fp.readlines():
# 如果某一行是以":"结尾,则跳过这一行,相当于是跳过第一行
if line.strip().endswith(":"):
continue
userID, _, _ = line.split(",") # 按逗号进行切分,得到用户的ID
users.add(userID) # 将用户的ID添加到用户的集合当中
return random.sample(list(users), 1000) # 从列表中随机返回1000个用户
编写加载数据函数
def load_and_split_data(self):
"""
加载数据,并将数据划分为训练集和测试集
:return:
"""
# 训练集和测试集都以字典形式保存
train = dict()
test = dict()
# 如果数据已经存在了,则直接加载
if os.path.exists("data/train.json") and os.path.exists("data/test.json"):
print("数据存在,从文件中加载数据!")
# json.load表示从json文件中加载数据,加载之前需要先用open函数打开文件
train = json.load(open("data/train.json"))
test = json.load(open("data/test.json"))
# 如果数据不存在,则要进行切分数据的工作
else:
print("数据不存在,读取数据以划分训练集和测试集!")
random.seed(self.seed) # 设置产生随机数的种子,保证每次试验产生的随机结果一致
for file in os.listdir(self.file_path):
one_path = "{}/{}".format(self.file_path, file) # 取出每一个数据文件的完整路径
print("当前路径: {}".format(one_path))
with open(one_path, "r") as fp:
movieID = fp.readline().split(":")[0] # 读取第一行,随后切割,取出切割的第一个值,获得电影的ID
# 遍历所有行
for line in fp.readlines():
# 跳过第一行
if line.endswith(":"):
continue
userID, rate, _ = line.split(",") # 获取用户的ID,及评分
# 判断用户是否在所选择的1000个用户当中
if userID in self.users:
print(userID, rate)
# 将用户均分成训练集和测试集
if random.randint(1, 50) == 1:
# 设置默认值,字典形式,评分向下取整||形式:{"user1":{"movie1":1,"movie2":2,...},...}
test.setdefault(userID, {})[movieID] = int(rate)
else:
train.setdefault(userID, {})[movieID] = int(rate)
# json.dump表示将数据保存成json格式,第一个参数是要保存的数据,第二个参数的打开的文件(保存位置)
print(train)
print("---")
print(test)
# 如果data目录不存在,则先在当前目录下创建data目录,否则open()函数会报错
if not os.path.exists("data"):
os.mkdir("data")
json.dump(train, open("data/train.json", "w"))
json.dump(test, open("data/test.json", "w"))
return train, test
编写计算用户相似度函数:使用皮尔逊相关系数进行计算
def pearson(self, rating1, rating2):
"""
计算皮尔逊相关系数,来表示用户之间的相似度
:param rating1:
:param rating2:
:return: 返回的是相似度度量值
"""
# 计算皮尔逊相关系数所需要的
sum_xy = 0 # 用户1和用户2对相同电影的评分相乘
sum_x = 0 # 用户1和用户2相同电影评分中,用户1对这些电影的评分总合
sum_y = 0 # 用户1和用户2相同电影评分中,用户2对这些电影的评分总合
sum_x2 = 0 # 用户1和用户2相同电影评分中,用户1对这些电影的评分的平方的总和
sum_y2 = 0 # 用户1和用户2相同电影评分中,用户2对这些电影的评分的平方的总和
num = 0
# 循环遍历用户的数据
for key in rating1.keys():
# 如果两个用户同时对电影评分了
if key in rating2.keys():
num += 1 # 记录两个用户评分过的相同的电影的数量
x = rating1[key] # 得到用户1对这部电影的评分
y = rating2[key] # 得到用户2对这部电影的评分
sum_xy += x * y
sum_x += x
sum_y += y
sum_x2 += math.pow(x, 2)
sum_y2 += math.pow(y, 2)
# 如果两个用户没有一同评分过的电影,则相关系数为0
if num == 0:
return 0
# 计算皮尔逊相关系数的分母,分母要分开计算,因为如果两个变量的方差为0,则就没有意义了
denominator = math.sqrt(sum_x2 - math.pow(sum_x, 2) / num) * math.sqrt(sum_y2 - math.pow(sum_y, 2) / num)
# 如果相关系数的分母是0,则没有意义
if denominator == 0:
return 0
else:
return (sum_xy - (sum_x * sum_y) / num) / denominator
测试1:测试用户相关系数
# 调用相关系数测验 这里的411705和452261都是train数据集中的用户ID(根据自己train中的数据),
# 得出结果是0.2981122155377652
# First是类实例化完成的对象
print(FirstS.pearson(FirstS.train['411705'], FirstS.train['452261']))
编写电影推荐函数
def recommend(self, userID):
"""
为用户ID为userID的用户进行电影推荐
:param userID:
:return: 返回值是用字典dict()存储的电影
"""
neighborUser = dict() # 用字典形式存储用户距离(相关系数)
# 遍历训练集的所有用户
for user in self.train.keys():
# 保证不是同一个用户
if userID != user:
distance = self.pearson(self.train[userID], self.train[user]) # 计算相关系数
neighborUser[user] = distance # 用字典存储用户之间的相关系数(距离)
# 例:dict_items([('小明', 1), ('小北', 2)]) ,k相当于就是(元组),k[1]则是取出评分来进行排序
newNeighborUser = sorted(neighborUser.items(), key=lambda k: k[1], reverse=True) # 根据距离来进行字典排序
movies = dict() # 用字典来存储推荐的电影以及推荐分
# 之前已经按照升序排序了,这里取出评分最高的k个来,即挑选出k个近邻用户个数
for (sim_user, sim) in newNeighborUser[:self.k]:
# sim_user是ID,sim是用户相似度
# 遍历该用户的所有电影
for movieID in self.train[sim_user].keys():
movies.setdefault(movieID, 0) # 如果该电影之前还不存在,则设置默认值0
# 计算用户对电影的感兴趣程度,使用"距离"即用户相关系数 * 另一个用户对该电影的评分,总和要加起来
movies[movieID] += sim * self.train[sim_user][movieID]
# 将推荐出来的电影,按照推荐分(感兴趣程度)进行排序
newMovies = sorted(movies.items(), key=lambda k: k[1], reverse=True)
return newMovies
测试2:测试电影推荐结果
# 为用户411705进行电影推荐 得出结果 [('17292', 10.062177826491071), ('2660', 6.6388284833811735), ('15755', 6.252865699153531)..
# 第一个结果是电影的ID,第二个结果是推荐分
print(FirstS.recommend("411705"))
编写评估函数
def evaluate(self, num=30):
"""
评估:针对测试集,为其推荐的电影占本身有行为电影的比例
:param num: 表示为随机的30个用户推荐电影,并查看其准确率
:return:
"""
print("开始计算准确率!")
precisions = list() #
random.seed(10) # 设置随机数种子
# 随机在测试集中获取30个用户
for userID in random.sample(self.test.keys(), num):
hit = 0
result = self.recommend(userID)[:self.n_items] # 为每个用户推荐电影,n_items是推荐的电影数,取出排名靠前的
# 遍历推荐列表
for (item, rate) in result:
# 如果推荐出来的电影,在测试集中,有被操作过,则记录加一
if item in self.test[userID]:
hit += 1
precisions.append(hit / n_items) # 操作过的/推荐出来的 = 准确率
# 计算准确率的平均值 并返回
return sum(precisions) / len(precisions)
测试3:测试推荐结果
# 测试推荐效果, 使用准确率 结果 0.0033333333333333335 可以通过修改参数,如选取的近邻用户个数,来进行模型调优!
print(FirstS.evaluate())
总结
本文介绍了协同过滤算法的两种分类,以及使用余弦相似度度量和基于皮尔逊相关系数度量的用户协同过滤算法。祝学习愉快!