本文将介绍协同过滤,一种广泛使用在自动推荐系统中的技术(但不局限于推荐系统)。本文还讨论如何理解一种嵌入的更为广泛的概念。
第一部分介绍协同过滤基本观点
在推荐系统中,协同过滤是一种通过分析用户品味类似于其他用户做出的分析,从而推荐用户其他感兴趣的物品。通过协同多视角的过滤模式的思想称为协同过滤。
协同过滤的潜在假设是当A和B在一个问题上有相同的观点时,A很可能在其他问题上跟B也有相同的观点。
实际场景:给定不同用户对不同电影进行的打分0-5。空格表示缺失值。我们的目标是尽可能把表格填充完整:协同过滤和嵌入
建模成:协同过滤和嵌入
在上图中可以看到完整框架。给定具体例子去理解Jon-Forrest Gump
模型预测的rating(3.25) = 两嵌入向量点乘 + 0.14 (电影偏置)+0.87(用户偏置)。基于这种rating,我们计算每个RMSE损失,通过比较预测rating和真实rating。
很明显这种预测是基于两个嵌入矩阵和2个偏置矩阵。但关于这矩阵中的数值,看起来是随机生成的,实际上是随机初始化的。那么什么样的数字在预测时是正确的呢?
我们通过使用一些优化算法(梯度下降)最小误差函数来学习这些数值。
嵌入:
关键思想1:找到每个用户和每个电影的合适表达作为嵌入。
Jon和Forrest Gump使用向量表达(图中使用5个数值表示)这些向量称为嵌入。在这个5维向量空间中,Jon和Forrest Gump被两个嵌入向量表示。
协同过滤和嵌入
Embedding 就是对于一个特定实体使用多维向量来表示。
这种表达实体为高维向量的方法是关键。这种表达能够捕获不同实体之间不得复杂关系。
为什么维度设定为5?
没有特殊规则关于嵌入向量的维度,这需要实验验证。维度应该足够去捕获实体的复杂程度。1维和2维向量可能不能捕获Jon的复杂度。但也不能太高。
在嵌入向量数值背后的直觉
我们可以认为这些数值作为捕获不同实体的特征。例如Jon第一个数1.34可能表示他多喜欢看幻想小说。在Forrest Gump第一个数-1.72告诉我们我们认为它是幻想类的程度。
理论上来说这是一种很好的方法去想嵌入向量中的数值,但真实中不能解释每个数值捕获的是什么。
在嵌入空间的亲密度
关键思想2:若一个电影和一个用户在向量空间比较接近那么该用户对这个电影评分较高。
有许多种方法来捕获相似度:点乘,欧式距离,余弦相似度。本文使用点乘。
偏置的角色
关键点3:实体的天性是其于其他实体的交互是相互独立的。
换句话说一些用户是很严肃的。类似一些电影可以被认为是很好,将会由大部分用户评分较高。这就是由偏置捕获的信息。
对这个模型的推荐
现在我们有一个训练好的模型,其学习了正确的嵌入和偏置对于每个用户和电影。当我们需要对一个用户推荐一系列电影时(假设该用户所有电影都没看过)。使用这种嵌入和偏置我们可以预测用户对每个电影的评分。推荐的电影为最高预测的评分。
预测评分=嵌入向量点乘(user, movie) + 用户偏置+电影偏置。
接下来介绍如何实现使用fastai库实现协同过滤。这个库使用Pytorch实现。

数据集准备:
Movielens 数据集(ml-latest-small)这个数据集描述5星评分和文本标记活动,包含100004个评分和1296标记应用在9125电影上,有671位用户,时间为1995-1-09 到2016-10-16。使用的文件名ratings.csv 和movies.csv。

使用fastai的协同过滤
首先需要两件东西:GPU和安装fastai库(pip install fastai)

step1: 加载数据
import torch
import pandas as pd
from bokeh.plotting import figure, show, output_notebook, save
from bokeh.models import HoverTool, value, LabelSet, Legend, ColumnDataSource
output_notebook()
from fastai.learner import *
from fastai.column_data import *
path =’movielens/ml-latest-small/’
ratings = pd.read_csv(f’{path}ratings.csv’)
movies = pd.read_csv(f’{path}movies.csv’)
ratings = ratings.drop([‘timestamp’], axis=1)
# 丢掉时间戳变量

现在查看ratings文件和movies文件:协同过滤和嵌入协同过滤和嵌入

电影文件中包含了关于电影的元数据。
第二步 建模训练集

val_idxs = get_cv_idxs(len(ratings)) #fastai function得到验证集的下标

我们将数据集划分成训练集和验证集,验证集是原始数据集的20%。我们使用权重衰减来防止过拟合,并且定义嵌入向量的维度。

wd = 2e-4 # weight decay
n_factors = 50# 嵌入向量的维度
cf = CollabFilterDataset.from_csv(path, ‘ratings.csv’, ‘userId’, ‘movieId’, ‘rating’) # fastai function 创建本地文件加载器
learn = cf.get_learner(n_factors, val_idxs, 64, opt_fn=optim.Adam) # 创建一个学习器(模型)确定batch_size大小和优化器函数

learner函数有以下参数:n_factors:表示嵌入向量的维度(本例子中为50)
val_idxs:从ratings.csv文件中读取的行下标,当做验证集。
batch_size:对梯度下降每步中输入到优化器中的样本个数。本例子中每次迭代使用64行。
opt_fn选择使用的优化器。库中包括ASGD,Adadelta, Adagrad, Adam, Adamax, LBFGS, Optimizer, RMSprop, Rprop, SGD优化函数。

learner.fit(1e-2, 2, wds=wd, cycle_len=1,cycle_mult=2, use_wd_sched =True) #调用learner中的fit函数训练模型和并学习嵌入和偏置矩阵中的正确值。

fit函数中参数:
learning_rate:1e-2是我们对于优化函数使用的学习率。wd是权重衰减。cycle_len/ cycle_mult: 这是fastai嵌入最好的学习率安排。
use_wd_sched:是否对权重衰减使用schedule。

第三步:在验证集上预测

preds=learn.predict() # 在验证集上预测
math.sqrt(metrics.mean_squared_error(y, preds)) # 计算RMSE
y = learn.data.val_y # 验证集上真实评分
sns.jointplot(preds, y, kind=’hex’, stat_func=None) #画图

现在我们试图解读嵌入和偏置的含义并观察是否能捕获一些有意义的信息。

解读嵌入和偏置
电影嵌入:我们嵌入向量设定维度为50。我们使用TSNE,是一种有效的方法去可视化数据在更低维空间,同时保持向量间的空间关系。

from sklearn.manifold import TSNE
movies = pd.read_csv(f’{path}movies.csv’) # 加载电影文件
movie_names=movies.set_index(‘movieId’)[‘title’].to_dict() #创建关于movieId的字典: movie title
g=ratings.groupby(‘movieId’)[‘rating’].count()# 对每个电影计数评分了的个数
topMovies=g.sort_values(ascending=False).index.values[:3000]# 找到前3000个评分数最多的电影
topMovieIdx=np.array([cf.item2idx[0] for 0 in topMovies]) #找到top电影的id链接到由模型产生的嵌入和偏置矩阵
m=learn.model; m.cuda()#检索模型并转化到GPU
movie_emb=to_np(m_i(V(topMovieIdx)))#转换嵌入矩阵到Numpy矩阵
###减少电影嵌入维度从50到2
tsne=TSNE(n_components=2, verbose=1,perplexity=30,n_iter=1000,learning_rate=10)# n_components表示要降低到多少维,verbose(逻辑值默认False)表示更新过程是否打印, perplexity可以被认为是一种平滑对量关于近邻数,一般设定在5-50,learning_rate一般在100-1000之间
tsne_results=tsne.fit_transform(movie_emb)# fit_transform函数完成了两步拟合加转化
#可视化

在降维后空间我们可以看到来自相同策略的电影分布很密集,这显示了嵌入不仅仅是为了减少误差而优化的。
我们期望来自相同类别的电影在流派、导演等其他特征上或多或少要相似。在嵌入空间中相近表示了其嵌入捕获了这些特性。
电影偏置:
电影偏置可以被认为是实际电影好坏的一种度量,针对不同用户的不同打分模型调整。偏置可以被认为是电影实际好、流行的程度的一种代理。

movie_bias=to_np(m.ib(V(topMoiveIdx)))# 提取电影偏置并转化到numpy数组
movie_ratings=[(b[0], movie_names[i]) for i, b in zip(topMovies, movie_bias)]
sorted(movie_ratings, key=lambda o:o[0])[:15] # 找到基于偏置排序中最差的15个电影
sorted(movie_ratings,key=lambda o:o[0], reverse=True)[:15]#基于偏置排序的最好的15个电影

基于偏置排序电影更能说得过去,平均来自所有用户给定的评分。

嵌入聚类(基于评分)
下面挑选用户ID为547的用户为例,他有最高的电影评分个数。现在试图去看在评分和电影嵌入向量之间找到可视化关系。

user_547=ratings[ratings[‘userId’]=547]
u547MovieIdx=np.array([cf.item2idx[0] for o in user_547.movieId])
u547Ratings=user_547.rating

https://towardsdatascience.com/collaborative-filtering-and-embeddings-part-2-919da17ecefb
https://towardsdatascience.com/various-implementations-of-collaborative-filtering-100385c6dfe0

相关文章: