基于近邻用户的协同过滤
基于近邻用户的协同过滤即UserCF(User Collaborative Filtering),UserCF用于给用户 A 推荐和他有着相似观影兴趣的用户 B 喜欢观看的电影:
用户 A 的好友用户 B 喜欢看电影 2、3、4,恰好电影 3 和电影 4 用户 A 没有看过,所以就可以把电影 3 和电影 4 推荐给用户 A;
为什么说用户 A 和用户 B 的观影兴趣相似(或者说为什么A和B可以成为好友)?来自谚语:物以类聚人以群分。既然 A 和 B 能够成为好朋友,那么他们必然就有着某些共同的价值观和兴趣爱好,比如A和B都看过电影2;
协同过滤
协同过滤是一类算法的总称,这类算法是根据用户的行为数据给用户产生推荐结果的一类算法;
上面的背景中,根据用户的观影记录,即每个用户看过哪些电影,来进行电影推荐,所以它属于协同过滤的一种;
既然可以将用户 B 喜欢(看过)的电影推荐给用户 A,那么用户 A 喜欢的电影也可以推荐给用户 B。所以,基于近邻用户的协同过滤算法是在观影兴趣相似的用户间互相推荐电影:
梳理一下UserCF的流程:
比如需要给用户 A 推荐电影,那么首先要找到和用户 A 观影兴趣最相似的 K 个用户,然后再从这 K 个用户喜欢的电影中,找到用户 A 没有看过的电影,推荐给 A,
先不考虑相似度的计算方法,K=3 的情况下,和用户 A 最相似的 3 个用户依次是用户 B、C、D,从这 3 个用户喜欢的电影集合中过滤掉用户 A 看过的电影,然后计算 A 对剩下的电影感兴趣的程度,从中选取最感兴趣的 3 个电影推荐给用户 A 。这里的推荐数量可以根据产品需求来设定,不一定是 3;
感兴趣程度计算方法是将每个电影上有观看行为的用户相似度求和得到的,例如 A 对电影 2 的感兴趣程度为:
用户 B 的 0.8 + 用户 C 的 0.6 = 1.4
基于近邻物品的协同过滤
推荐系统根据用户过往看过的电影,从电影库中查找相似的电影推荐出来,这种方法叫做基于近邻物品的协同过滤算法(简称ItemCF);
图中,电影1和电影2是两部相似电影;
- 用户 A 看过电影 1,那么就给他推荐相似的电影 2;用户 D 看过电影 2,那么就给他推荐相似的电影 1。
- 电影 1 和电影 2 相似是因为他们有着共同的观影群体(比如B和C)。
- 因为用户 B 看过电影 1 和电影 2,所以只能给用户 B 推荐和电影 1 或电影 2 相似的电影,而不是推荐电影 1 和电影 2 本身。
梳理一下ItemCF的流程:
比如我们要给用户 A 推荐电影,首先要在用户 A 喜欢的电影中分别找到 K 个最相似的电影,然后再从这些电影中找到用户 A 没看过的电影推荐给 A ,
感兴趣程度计算方法是将每个候选电影上的电影相似度求和得到的,例如 A 对电影 4 的感兴趣程度为:电影 4 和电影 1 的相似度 0.5 + 电影 4 和电影 2 的相似度 0.4 = 0.9
近邻用户
- 兴趣相似的用户,比如两个用户都看过同一部电影
近邻物品
- 用户群体相似的物品,比如两部电影存在同一个观影群体
相似度计算-Jaccard相似度
在前面两个算法中,发现:
- UserCF需要计算用户的相似度
- ItemCF需要计算物品的相似度
在推荐系统中,使用KNN寻求到前K个相似用户(或者电影)时,即所谓KNN电影推荐系统;
相似度计算在第三课中提到过欧氏距离,这次新增杰卡德(Jaccard)相似度,杰卡德相似度是指两个集合的交集元素个数在并集中所占的比例;
比如整理得到以下数据:
图中展现的是代表用户观影记录的行为矩阵,矩阵中的 1 表示用户看过对应的电影,0 表示没看过;
如果用Jaccard计算用户相似度,应该为:
以用户B和D的相似度(或者用户D和B):
1
3
\frac{1}{3}
31,解释一下计算流程;
用户B和D共同看过电影2,交集为1,看过电影的并集是电影2和3和4,即并集为3,所以杰卡德相似度为
1
3
\frac{1}{3}
31
现在计算的是用户相似度,对于其中的某个用户,后期便可以根据前K个与之相似的用户完成电影推荐;
实验:基于KNN的电影推荐系统
简介
实验基于 Surprise 库及其中的 movielens 数据集实现一个简单的电影推荐系统;
实验很简单,模型直接使用 surprise 中的 KNNBaseline 实现基于近邻电影(物品)的协同过滤算法:根据用户最近喜欢看的电影推荐相似的 K 部电影;
Surprise,地址:可以基于显式评分数据(如movielens数据)实现一些常见推荐算法;
安装 Surprise 库,命令行输入:
conda install scikit-surprise
movielens 数据集
实验使用的 movielens 1M 数据集(1M是指共计100万评分记录),包含了6040名用户对大约3900部电影的1000209条评分记录。Surprise 中已经内置了这个数据集,初次使用时会先提示下载(输入Y进行下载):
# 导入Dataset类
from surprise import Dataset
# 调用函数,加载内置的 movielens 1m 数据集
data = Dataset.load_builtin('ml-1m')
下载完成后,可以查看 Surprise 的内置数据集在机器上的存放路径:
# 导入相关函数
from surprise import get_dataset_dir
# 获取数据集根目录,返回字符串
data_root = get_dataset_dir()
data_root
"""
'C:\\Users\\baijingyi/.surprise_data/'
"""
查看 movielens 1M 数据集包含的文件:
import os
file_dir = data_root + '/ml-1m/ml-1m/'
os.listdir(file_dir)
"""
['movies.dat', 'ratings.dat', 'README', 'users.dat']
"""
用pandas.read_table
查看三个*.dat
文件:
1.评分数据
# 使用 pandas 进行数据展示和分析
import pandas as pd
# 评分数据的文件路径
ratings_path = data_root + "/ml-1m/ml-1m/ratings.dat"
# 评分数据表的列名
ratings_col = ['userid', 'movieid', 'rating', 'timestamp']
# 读取数据表
ratings = pd.read_table(ratings_path,sep="::",engine='python',header=None,names=ratings_col)
# 展示前5行数据
ratings.head()
print('评分数量:',len(ratings))
# 展示各列的取值情况:取值的个数、数据类型
ratings.nunique()
userid movieid rating timestamp
0 1 1193 5 978300760
1 1 661 3 978302109
2 1 914 3 978301968
3 1 3408 4 978300275
4 1 2355 5 978824291
评分数量: 1000209
userid 6040
movieid 3706
rating 5
timestamp 458455
dtype: int64
2.用户属性数据
# 用户属性数据的文件路径
user_path = data_root + "/ml-1m/ml-1m/users.dat"
# 用户数据表的列名
user_col = ['userid', 'gender', 'age', 'occupation', 'zip-code']
# 读取数据表
users = pd.read_table(user_path,sep="::",engine='python',header=None,names=user_col)
# 展示前5行数据
users.head()
print('用户数量:',len(users))
# 展示各列的取值情况:取值的个数、数据类型
users.nunique()
userid gender age occupation zip-code
0 1 F 1 10 48067
1 2 M 56 16 70072
2 3 M 25 15 55117
3 4 M 45 7 02460
4 5 M 25 20 55455
用户数量: 6040
userid 6040
gender 2
age 7
occupation 21
zip-code 3439
dtype: int64
3.电影属性数据
# 电影属性数据的文件路径
movies_path = data_root + "/ml-1m/ml-1m/movies.dat"
# 电影数据表的列名
movies_col = ['movieid', 'title', 'genres']
# 读取数据表
movies = pd.read_table(movies_path,sep="::",engine='python',header=None,names=movies_col)
# 展示前5行数据
movies.head()
print('电影数量:',len(movies))
# 展示各列的取值情况:取值的个数、数据类型
movies.nunique()
movieid title genres
0 1 Toy Story (1995) Animation|Children's|Comedy
1 2 Jumanji (1995) Adventure|Children's|Fantasy
2 3 Grumpier Old Men (1995) Comedy|Romance
3 4 Waiting to Exhale (1995) Comedy|Drama
4 5 Father of the Bride Part II (1995) Comedy
电影数量: 3883
movieid 3883
title 3883
genres 301
dtype: int64
定义映射函数read_item_names
,返回 “电影ID”<-->“电影名称”
的字典:
{ID("string"):name("string")}
{name("string"):ID(int)}
注意pandas的结果:movies.dat 中的 movieid 和 title 这两列分别对应了电影ID和电影名称:
def read_item_names():
# 电影属性数据的路径
file_name = data_root+"/ml-1m/ml-1m/movies.dat"
# 电影ID为key的字典
rid_to_name = {}
# 电影名称为key的字典
name_to_rid = {}
# ToDo
# 电影数据表的列名
movies_col = ['movieid', 'title', 'genres']
# 读取数据表
movies = pd.read_table(file_name, sep="::", engine='python', header=None, names=movies_col)
movieid=movies["movieid"].values
moviename=movies["title"].values
for i in range(len(movies)):
rid_to_name.update({str(movieid[i]):moviename[i]})
name_to_rid.update({moviename[i]:movieid[i]})
return rid_to_name, name_to_rid
# 返回映射字典
rid_to_name, name_to_rid = read_item_names()
print(rid_to_name['101']) # Bottle Rocket (1996)
print(name_to_rid['Kids of the Round Table (1995)']) # 56
模型实现
构建训练数据:
trainset = data.build_full_trainset()
使用 surprise 中的 KNNBaseline 实现基于近邻电影的协同过滤算法:
from surprise import KNNBaseline
# 使用皮尔逊系数计算 item 相似度
sim_options = {'name': 'pearson_baseline', 'user_based': False}
# 实例化 KNNBaseline
algo = KNNBaseline(sim_options=sim_options)
计算物品(电影)相似度:
# 计算相似度矩阵
algo.fit(trainset)
# 查看相似度矩阵
algo.sim
"""
array([[ 1. , 0.01532195, 0.00496607, ..., 0. ,
0. , 0. ],
[ 0.01532195, 1. , -0.06201139, ..., 0. ,
0. , 0. ],
[ 0.00496607, -0.06201139, 1. , ..., 0. ,
0. , 0. ],
...,
[ 0. , 0. , 0. , ..., 1. ,
0. , 0. ],
[ 0. , 0. , 0. , ..., 0. ,
1. , 0. ],
[ 0. , 0. , 0. , ..., 0. ,
0. , 1. ]])
"""
获取近邻电影的名称:
# 看过的电影名称
seen_movie = 'Heat (1995)'
# 转换为movieid
movieid = str(name_to_rid[seen_movie])
print(movieid)
# 转换为训练集内部id
inner_id = algo.trainset.to_inner_iid(movieid)
# 获取近邻电影的训练集内部id,近邻电影数 K = 10
neighbors_inner_id = algo.get_neighbors(inner_id, k=10)
# 获取近邻电影的movieid
neighbors_movie_id = [str(algo.trainset.to_raw_iid(i)) for i in neighbors_inner_id]
print(neighbors_movie_id)
# 获取近邻电影的名称
neighbors_movie_name = [rid_to_name[name] for name in neighbors_movie_id]
print(neighbors_movie_name)
即推荐结果为:
看过电影的ID:
6
近邻电影的ID:
['2231', '16', '431', '2952', '3006', '2023', '1892', '574', '3863', '1600']
近邻电影的名称:
['Rounders (1998)', 'Casino (1995)', "Carlito's Way (1993)", 'Hard 8 (a.k.a. Sydney, a.k.a. Hard Eight) (1996)', 'Insider, The (1999)', 'Godfather: Part III, The (1990)', 'Perfect Murder, A (1998)', 'Spanking the Monkey (1994)', 'Cell, The (2000)', "She's So Lovely (1997)"]