- 2018-6-17 更新推荐方法接口
一个简单的电影推荐系统。
模型移植至仓库https://github.com/chengstone/movie_recommender,原作者使用的是TensorFlow-1.0,本仓库得到作者允许后,使用PyTorch-0.4.0的移植版本。这里要感谢原作者对于框架部分的详细实现说明,使得我在复现过程中省去了很多麻烦。
模型使用了 movieLens-1m 的数据集进行训练,建模用户-电影-评分数据,欲实现给定用户和电影ID,预测用户对于电影的评分。在推荐系统领域,就是一个 rating 的任务。
注:图片来源原作者仓库
模型基本就是使用双通道网络来实现推荐系统领域比较有效的基于 rating 的 SVD++ 模型。这里简单讲下什么是SVD++
SVD++:
首先SVD++是SVD的推广。简单来说,SVD就是使用奇异值矩阵分解的方法,将推荐系统中的两个输入:用户矩阵和物品矩阵映射到隐藏空间,得到两个 dense 的隐藏向量,作为用户和物品的潜在喜好特征,之后对这两个向量进行点乘,获得最终的物品打分。那么++就是说,在考虑隐藏向量的时候,同时考虑一些辅助信息的作用,比如用户方面的用户个人信息等等。
在说明具体实现前,先来讲下代码的构成。
代码很简单,一共5个文件(4个代码文件+1个数据文件)
-
data.p: 保存了输入数据的pickle文件,加载完毕后是一个pandas(>=0.22.0)的DataFrame对象(如图)
-
dataset.py: 包含
torch.utils.data.Dataset
类,提供给torch.utils.data.Dataloader
加载数据 -
model.py: Pytorch对于上述模型的实现
-
main.py: 主文件。提供模型训练等操作
-
recInterface.py: 推荐方法接口,包含KNN等推荐方法
模型为用户-电影双通道。
- 用户通道: 主要是对用户ID、性别、职业等属性进行特征抽取,主要用到了
torch.nn.Embedding
对这些索引值进行 look up。分别获得不同属性的特征,之后通过两层全连接综合这些属性特征,获得一个总的用户特征。 - 电影通道: 一边同样使用look up获得电影id和类型的嵌入。一边则使用文本卷积网络提取电影名的特征,最后用全连接获得最终的电影特征。
最后对两个特征进行点积操作获得评分。
可以看到这里用到了很多次的Embedding
,所以在讲具体的代码实现前,先声明两个模型使用到的词典,来保存不同 Embedding
需要的最大值参数。
user_max_dict={
'uid':6041, # 用户ID的最大值
'gender':2, # 0/1 表示男女
'age':7, # 7个年龄段
'job':21 # 21个工作分类
}
movie_max_dict={
'mid':3953, # 电影ID的最大值
'mtype':18, # 电影类型的最大值
'mword':5215 # 电影名的词典term总数
}
同时,模型用到了文本卷积网络。原作者在实现的过程中,用了4个不同的卷积核,所以定义一下卷积核参数
convParams={
'kernel_sizes':[2,3,4,5]
}
用户通道主要进行连个操作: Embedding 和 全连接。所以在自定义的网络类中,预先定义如下网络层:
# --------------------------------- user channel --------------------------------------------
# user embeddings
self.embedding_uid = nn.Embedding(user_max_dict['uid'], embed_dim)
self.embedding_gender = nn.Embedding(user_max_dict['gender'], embed_dim // 2)
self.embedding_age = nn.Embedding(user_max_dict['age'], embed_dim // 2)
self.embedding_job = nn.Embedding(user_max_dict['job'], embed_dim // 2)
# user embedding to fc: the first dense layer
self.fc_uid = nn.Linear(embed_dim, embed_dim)
self.fc_gender = nn.Linear(embed_dim // 2, embed_dim)
self.fc_age = nn.Linear(embed_dim // 2, embed_dim)
self.fc_job = nn.Linear(embed_dim // 2, embed_dim)
# concat embeddings to fc: the second dense layer
self.fc_user_combine = nn.Linear(4 * embed_dim, fc_size)
最后一步连接所有的 attribute embedding,做一个全连接
part 1: attribute embedding
操作和用户通道类似:
# movie embeddings
self.embedding_mid = nn.Embedding(movie_max_dict['mid'], embed_dim) # normally 32
self.embedding_mtype_sum = nn.EmbeddingBag(movie_max_dict['mtype'], embed_dim, mode='sum')
self.fc_mid = nn.Linear(embed_dim, embed_dim)
self.fc_mtype = nn.Linear(embed_dim, embed_dim)
# movie embedding to fc
self.fc_mid_mtype = nn.Linear(embed_dim * 2, fc_size)
part2: textCNN
文本卷积网络,根据卷积核个数,定义几个卷积子网络即可:
# wordlist to embedding matrix B x L x D L=15 15 words
self.embedding_mwords = nn.Embedding(movie_max_dict['mword'], embed_dim)
# input word vector matrix is B x 15 x 32
# load text_CNN params
kernel_sizes = convParams['kernel_sizes']
# 8 kernel, stride=1,padding=0, kernel_sizes=[2x32, 3x32, 4x32, 5x32]
self.Convs_text = [nn.Sequential(
nn.Conv2d(1, 8, kernel_size=(k, embed_dim)),
nn.ReLU(),
nn.MaxPool2d(kernel_size=(15 - k + 1, 1), stride=(1, 1))
).to(device) for k in kernel_sizes]
# movie channel concat
self.fc_movie_combine = nn.Linear(embed_dim * 2 + 8 * len(kernel_sizes), fc_size) # tanh
main.py
里main函数的注释部分基本就足够运行程序了。
-
训练模型
# 构建模型 model = rec_model(user_max_dict=user_max_dict, movie_max_dict=movie_max_dict, convParams=convParams) model=model.to(device) # 训练模型 train(model=model,num_epochs=1) # num_epochs控制训练轮数 # 保存训练后的模型参数 torch.save(model.state_dict(), 'Params/model_params.pkl')
-
保存用户和电影特征
调用
recInterface.py
里的接口。实际上就是将数据输入一遍已经训练好的网络,保存中间结果到本地。# 同样是先构建模型 model = rec_model(user_max_dict=user_max_dict, movie_max_dict=movie_max_dict, convParams=convParams) model=model.to(device) # 加载本地保存好的模型参数 model.load_state_dict(torch.load("Params/model_params.pkl")) # 调用接口 from recInterface import saveMovieAndUserFeature saveMovieAndUserFeature(model=model)
-
使用recInterface获取相关推荐
注意:使用这个接口的前提是已经保存了用户和电影特征
目前只有两个功能:
-
getKNNitem(itemID,itemName='movie',K=1)
:获取K近邻的用户或者电影。默认为电影,默认的K为1 -
getUserMostLike(uid)
: 获取id为uid的用户最喜欢的电影(cosine距离最小的那个)# 调用接口 from recInterface import getKNNitem, getUserMostLike itemlist = getKNNitem(itemID=100,K=10) movieID = getUserMostLike(uid=100)
-