行业报告 AI展会 数据标注 标注供求
数据标注数据集
主页 > 机器学习 正文

推荐系统遇上深度学习 (十一)-- 神经协同过滤

看到了一篇关于神经协同过滤的论文,感觉还不错,跟大家分享下。

论文地址:

https://www.comp.nus.edu.sg/~xiangnan/papers/ncf.pdf

1、Neural Collaborative Filtering

1.1 背景

本文讨论的主要是隐性反馈协同过滤解决方案,先来明确两个概念:显性反馈和隐性反馈:

显性反馈行为包括用户明确表示对物品喜好的行为
隐性反馈行为指的是那些不能明确反应用户喜好

举例来说:

很多应用场景,并没有显性反馈的存在。因为大部分用户是沉默的用户,并不会明确给系统反馈“我对这个物品的偏好值是多少”。因此,推荐系统可以根据大量的隐性反馈来推断用户的偏好值。

根据已得到的隐性反馈数据,我们将用户 - 条目交互矩阵 Y 定义为:

但是,Yui 为 1 仅代表二者有交互记录,并不代表用户 u 真的喜欢项目 i,同理,u 和 i 没有交互记录也不能代表 u 不喜欢 i。这对隐性反馈的学习提出了挑战,因为它提供了关于用户偏好的噪声信号。虽然观察到的条目至少反映了用户对项目的兴趣,但是未查看的条目可能只是丢失数据,并且这其中存在自然稀疏的负反馈。

在隐性反馈上的推荐问题可以表达为估算矩阵 Y 中未观察到的条目的分数问题(这个分数被用来评估项目的排名)。形式上它可以被抽象为学习函数:

为了处理缺失数据,有两种常见的做法:要么将所有未观察到的条目视作负反馈,要么从没有观察到条目中抽样作为负反馈实例。

1.2 矩阵分解及其缺陷

传统的求解方法是矩阵分解 (MF,Matrix Factorization),为每个 user 和 item 找到一个隐向量,问题变为:

这里的 K 表示隐式空间(latent space)的维度。正如我们所看到的,MF 模型是用户和项目的潜在因素的双向互动,它假设潜在空间的每一维都是相互独立的并且用相同的权重将它们线性结合。因此,MF 可视为隐向量(latent factor)的线性模型。

论文中给出了一个例子来说明这种算法的局限性:

1(a) 是 user-item 交互矩阵,1(b) 是用户的隐式空间,论文中强调了两点来理解这张图片:
1)MF 将 user 和 item 分布到同样的隐式空间中,那么两个用户之间的相似性也可以用二者在隐式空间中的向量夹角来确定。
2)使用 Jaccard 系数来作为真实的用户相似性。
通过 MF 计算的相似性与 Jaccard 系数计算的相似性也可以用来评判 MF 的性能。我们先来看看 Jaccard 系数

上面的示例显示了 MF 因为使用一个简单的和固定的内积,来估计在低维潜在空间中用户 - 项目的复杂交互,从而所可能造成的限制。解决该问题的方法之一是使用大量的潜在因子 K (就是隐式空间向量的维度)。然而这可能对模型的泛化能力产生不利的影响(e.g. 数据的过拟合问题),特别是在稀疏的集合上。论文通过使用 DNNs 从数据中学习交互函数,突破了这个限制。

1.3 NCF

本文先提出了一种通用框架:

针对这个通用框架,论文提出了三种不同的实现,三种实现可以用一张图来说明:

GMF:
上图中仅使用 GMF layer,就得到了第一种实现方式 GMF,GMF 被称为广义矩阵分解,输出层的计算公式为:

MLP
上图中仅使用右侧的 MLP Layers,就得到了第二种学习方式,通过多层神经网络来学习 user 和 item 的隐向量。这样,输出层的计算公式为:

NeuMF
结合 GMF 和 MLP,得到的就是第三种实现方式,上图是该方式的完整实现,输出层的计算公式为:

1.4 模型实验

论文通过三个角度进行了试验:

RQ1 我们提出的 NCF 方法是否胜过 state-of-the-art 的隐性协同过滤方法?
RQ2 我们提出的优化框架(消极样本抽样的 logloss)怎样为推荐任务服务?
RQ3 更深的隐藏单元是不是有助于对用户项目交互数据的学习?

使用的数据集:MovieLens 和 Pinterest 两个数据集

评估方案:为了评价项目推荐的性能,论文采用了 leave-one-out 方法评估,即:对于每个用户,我们将其最近的一次交互作为测试集(数据集一般都有时间戳),并利用余下的培训作为训练集。由于在评估过程中为每个用户排列所有项目花费的时间太多,所以遵循一般的策略,随机抽取 100 个不与用户进行交互的项目,将测试项目排列在这 100 个项目中。排名列表的性能由命中率(HR)和归一化折扣累积增益(NDCG)来衡量。同时,论文将这两个指标的排名列表截断为 10。如此一来,HR 直观地衡量测试项目是否存在于前 10 名列表中,而 NDCG 通过将较高分数指定为顶级排名来计算命中的位置。本文计算每个测试用户的这两个指标,并求取了平均分。

Baselines,论文将 NCF 方法与下列方法进行了比较:ItemPop,ItemKNN,BPR,eALS。

以下是三个结果的贴图,关于试验结果的解读,由于篇幅的原因,大家可以查看原论文。

RQ1 试验结果

简单的结论,即 NCF 效果好于 BaseLine 模型,如果不好的话论文也不用写了,哈哈。

RQ2 试验结果

Figure 6 表示将模型看作一个二分类任务并使用 logloss 作为损失函数时的训练效果。
Figure7 表示采样率对模型性能的影响(横轴是采样率,即负样本与正样本的比例)。

RQ3 试验结果

上面的表格设置了两个变量,分别是 Embedding 的长度 K 和神经网络的层数,使用类似网格搜索的方式展示了在两个数据集上的结果。增加 Embedding 的长度和神经网络的层数是可以提升训练效果的。

2、NCF 实战

本文的 github 地址为:

https://github.com/princewen/tensorflow_practice/tree/master/recommendation/Basic-NCF-Demo

本文仅介绍模型相关细节,数据处理部分就不介绍啦。

项目结构如下:

数据输入
本文使用了一种新的数据处理方式,不过我们的输入就是三个:userid,itemid 以及 label,对训练集来说,label 是 0-1 值,对测试集来说,是具体的 itemid

def get_data(self):
sample = self.iterator.get_next()
   self.user = sample['user']
   self.item = sample['item']
   self.label = tf.cast(sample['label'],tf.float32)
 

定义初始化方式、损失函数、优化器

def inference(self):
    """ Initialize important settings """
    self.regularizer = tf.contrib.layers.l2_regularizer(self.regularizer_rate)

    if self.initializer == 'Normal':
        self.initializer = tf.truncated_normal_initializer(stddev=0.01)
    elif self.initializer == 'Xavier_Normal':
        self.initializer = tf.contrib.layers.xavier_initializer()
    else:
        self.initializer = tf.glorot_uniform_initializer()

    if self.activation_func == 'ReLU':
        self.activation_func = tf.nn.relu
    elif self.activation_func == 'Leaky_ReLU':
        self.activation_func = tf.nn.leaky_relu
    elif self.activation_func == 'ELU':
        self.activation_func = tf.nn.elu

    if self.loss_func == 'cross_entropy':
        # self.loss_func = lambda labels, logits: -tf.reduce_sum(
        #       (labels * tf.log(logits) + (
        #       tf.ones_like(labels, dtype=tf.float32) - labels) *
        #       tf.log(tf.ones_like(logits, dtype=tf.float32) - logits)), 1)
        self.loss_func = tf.nn.sigmoid_cross_entropy_with_logits

    if self.optim == 'SGD':
        self.optim = tf.train.GradientDescentOptimizer(self.lr,
                                                       name='SGD')
    elif self.optim == 'RMSProp':
        self.optim = tf.train.RMSPropOptimizer(self.lr, decay=0.9,
                                               momentum=0.0, name='RMSProp')
    elif self.optim == 'Adam':
        self.optim = tf.train.AdamOptimizer(self.lr, name='Adam')


得到 embedding 值
分别得到 GMF 和 MLP 的 embedding 向量,当然也可以使用 embedding_lookup 方法:

with tf.name_scope('input'):
   self.user_onehot = tf.one_hot(self.user,self.user_size,name='user_onehot')
   self.item_onehot = tf.one_hot(self.item,self.item_size,name='item_onehot')

with tf.name_scope('embed'):
   self.user_embed_GMF = tf.layers.dense(inputs = self.user_onehot,
units = self.embed_size,
activation = self.activation_func,
                                         kernel_initializer=self.initializer,
                                         kernel_regularizer=self.regularizer,
                                         name='user_embed_GMF')

   self.item_embed_GMF = tf.layers.dense(inputs=self.item_onehot,
                                         units=self.embed_size,
                                         activation=self.activation_func,
                                         kernel_initializer=self.initializer,
                                         kernel_regularizer=self.regularizer,
                                         name='item_embed_GMF')

   self.user_embed_MLP = tf.layers.dense(inputs=self.user_onehot,
                                         units=self.embed_size,
                                         activation=self.activation_func,
                                         kernel_initializer=self.initializer,
                                         kernel_regularizer=self.regularizer,
                                         name='user_embed_MLP')
   self.item_embed_MLP = tf.layers.dense(inputs=self.item_onehot,
                                         units=self.embed_size,
                                         activation=self.activation_func,
                                         kernel_initializer=self.initializer,
                                         kernel_regularizer=self.regularizer,
                                         name='item_embed_MLP')
										 

GMF
GMF 部分就是求两个 embedding 的内积:

with tf.name_scope("GMF"):
   self.GMF = tf.multiply(self.user_embed_GMF,self.item_embed_GMF,name='GMF')

MLP

with tf.name_scope("MLP"):
   self.interaction = tf.concat([self.user_embed_MLP, self.item_embed_MLP],
                                axis=-1, name='interaction')

   self.layer1_MLP = tf.layers.dense(inputs=self.interaction,
                                     units=self.embed_size * 2,
                                     activation=self.activation_func,
                                     kernel_initializer=self.initializer,
                                     kernel_regularizer=self.regularizer,
                                     name='layer1_MLP')
   self.layer1_MLP = tf.layers.dropout(self.layer1_MLP, rate=self.dropout)

   self.layer2_MLP = tf.layers.dense(inputs=self.layer1_MLP,
                                     units=self.embed_size,
                                     activation=self.activation_func,
                                     kernel_initializer=self.initializer,
                                     kernel_regularizer=self.regularizer,
                                     name='layer2_MLP')
   self.layer2_MLP = tf.layers.dropout(self.layer2_MLP, rate=self.dropout)

   self.layer3_MLP = tf.layers.dense(inputs=self.layer2_MLP,
                                     units=self.embed_size // 2,
                                     activation=self.activation_func,
                                     kernel_initializer=self.initializer,
                                     kernel_regularizer=self.regularizer,
                                     name='layer3_MLP')
   self.layer3_MLP = tf.layers.dropout(self.layer3_MLP, rate=self.dropout)
  

得到预测值

with tf.name_scope('concatenation'):
    self.concatenation = tf.concat([self.GMF,self.layer3_MLP],axis=-1,name='concatenation')


    self.logits = tf.layers.dense(inputs= self.concatenation,
                                  units = 1,
                                  activation=None,
                                  kernel_initializer=self.initializer,
                                  kernel_regularizer=self.regularizer,
                                  name='predict')

    self.logits_dense = tf.reshape(self.logits,[-1])


测试集构建
这里只介绍几行关键的测试集构建代码,整个流程希望大家可以看一下完整的代码。
需要明确的一点是,对于测试集,我们的评价不只是对错,还要关注排名,所以测试集的 label 不是 0-1,而是具体的 itemid
首先,对每个 user 取最后一行作为测试集的正样本:

split_train_test = []

for i in range(len(user_set)):
   for _ in range(user_length[i] - 1):
       split_train_test.append('train')
   split_train_test.append('test')

full_data['split'] = split_train_test

train_data = full_data[full_data['split'] == 'train'].reset_index(drop=True)
test_data = full_data[full_data['split'] == 'test'].reset_index(drop=True)

添加一些负采样的样本, 这里顺序是,1 正样本 -n 负样本 -1 正样本 -n 负样本….,每个用户有 n+1 条数据,便于计算 HR 和 NDCG:

feature_user.append(user)
feature_item.append(item)
labels_add.append(label)

for  k  in  neg_samples:
   feature_user.append(user)
   feature_item.append(k)
   labels_add.append(k)

不打乱测试集的顺序,设置 batch 的大小为 1+n:

dataset = tf.data.Dataset.from_tensor
_slices(data)
dataset = dataset.batch(test_neg + 1)

计算 HR 和 NDCG

def hr(gt_item, pred_items):
   if gt_item in pred_items:
       return  1
   return  0

def ndcg(gt_item, pred_items):
   if gt_item in pred_items:
       index = np.where(pred_items == gt_item)[0][0]
       return np.reciprocal(np.log2(index + 2))
   return 0

更详细的代码可以参考 github,最好能够手敲一遍来理解其原理哟!

参考文章

https://www.comp.nus.edu.sg/~xiangnan/papers/ncf.pdf
https://www.cnblogs.com/HolyShine/p/6728999.html

微信公众号

声明:本站部分作品是由网友自主投稿和发布、编辑整理上传,对此类作品本站仅提供交流平台,转载的目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,不为其版权负责。如果您发现网站上有侵犯您的知识产权的作品,请与我们取得联系,我们会及时修改或删除。

网友评论:

发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
用户名: 验证码:点击我更换图片
SEM推广服务

Copyright©2005-2028 Sykv.com 可思数据 版权所有    京ICP备14056871号

关于我们   免责声明   广告合作   版权声明   联系我们   原创投稿   网站地图  

可思数据 数据标注

扫码入群
扫码关注

微信公众号

返回顶部