论文笔记 | NAIS: Neural Attentive Item Similarity Model for Recommendation-TKDE2018

NAIS Neural Attentive Item Similarity Model for Recommendation-TKDE2018

论文链接:https://arxiv.org/pdf/1809.07053.pdf

论文代码地址:https://github.com/AaronHeee/Neural-Attentive-Item-Similarity-Model

这篇论文运用attention机制的地方在计算项目相似度,作者认为用户的历史商品对于推荐的目标商品所产生的影响是不同的,所以在计算用户对商品的预测评分时,根据用户的历史商品得到的用户特征应该根据影响因子作权重加和。在使用传统的softmax时发现效果不好,因为要计算影响因子的数量太大,最后得出的概率分布并不好,所以在softmax的分母添加了指数衰减因子

Attention model: 源自人类的视觉注意力机制,在观察一幅图像时对信息量高的高质量区域投入更多的注意力。通俗地讲就是通过对信息进行筛选,为质量高的信息分配更多的注意力也就是权重。user based CF: 基于物品的协同过滤,推荐系统的一种传统算法,通过用户行为计算物品相似度,为用户推荐和用户历史商品相似的物品。

介绍

传统的基于物品的协同过滤都是通过一些通用的相似度算法计算物品相似度,如cosine等。本文提出了一种定制的注意力框架来学习物品之间的相似度。对于目标商品,根据用户的历史商品来预测用户对目标商品的评分时,用户的历史商品所贡献的信息量是不同的,在传统算法时,预测公式采用相似度权重以及历史商品评分的加权和。本文对传统的相似度权重进行创新,设计了一种利用注意力机制的网络结构,通过实验证明可以更好的学习商品之间的相似度,提高推荐准确率。

1. 模型设计

预测模型

本文设计了两种注意力模型

  • 第一种,将历史商品特征和目标商品特征堆叠;

  • 第二种,求历史商品特征和目标商品特征的点积。注意力网络采用MLP结构。

损失函数:

结构图:

NAIS结构图

2. 实验设计

数据集:

实验结果:

3. 讨论

  • 实时个性化

本文设计的算法在支持实时更新用户特征上有着出色的表现。对于实时个性化,需要实时监控用户行为,用户在对某个商品交互后,实施推荐系统同时更新用户的推荐列表。因为重新训练整个模型不现实,一般都选择更新模型参数,然而因为用户行为可能并行发生,更新模型的固有参数会发生冲突,虽然可以通过分布式结构来解决但是分布式往往需要更多的消耗。本文的算法在实时问题上有更好的解决方式,首先用户的特征可以直接通过加法更新,时间消耗基本是常数级的。比如系统检测到用户u消耗了商品t,这时用户u对于商品i的预测为原预测值\hat{y_{ui}}的基础上加上a_{ij}p_i^Tq_t,根本不需要重新计算模型。

  • 两种注意力模型结构的选择

从公式中可以看出,本文设计了两种不同的注意力模型结构,一种是直接将p_i和q_j直接连接在一起,组成不同shape的特征矩阵,另一种则是计算p_iq_j的点乘。前者保留了商品特征的原始结构,但是因为矩阵的结构发生变化可能导致网络难以收敛。后者的矩阵结构满足学习的目标,但是丢失了学习的商品特征。两种结构各有利弊,也是作者设计两种模型的原因。

4. 结论

本文出发点为用户的历史行为商品对目标商品预测做出的贡献是不同的,本文接下来的工作就是将NAIS结合更加先进的深度模型提高推荐准确率,以及考虑推荐系统的可解释性。

相关工作

这部分作者介绍的很迷,花了几大段写推荐系统的任务从显示评分到隐式评分,分别采用不同的度量方式。介绍了一个最先进的排序方法,是对抗个性化排序。然后介绍了深度学习在推荐系统的应用,分了两个部分,一个是学习特征表示,另一个是学习scoring function。关于第二种介绍了三篇比较先进的论文。最后讨论了另一个采用attention的论文,这篇论文是基于用户的,并且强调了自己论文的亮点,想出了一个解决softmax计算大规模概率分布的方法。


代码分析

定义参数

  • path : 数据路径

  • dataset: 选择的数据集,pinterest 还是movielens

  • pretrain: 0: No pretrain, 1: Pretrain with updating FISM variables, 2:Pretrain with fixed FISM variables.

  • verbose: Interval of evaluation

  • batch_choice: user: generate batches by user, fixed:batch_size: generate batches by batch size

  • epochs: 周期数

  • weight_size: weight size

  • embed_size: Embedding size

  • data_alpha: Index of coefficient of embedding vector

  • regs: Regularization for user and item embeddings.

  • alpha: Index of coefficient of embedding vector

  • train_loss: Caculate training loss or nor

  • beta: Index of coefficient of sum of exp(A)

  • num_neg: Number of negative instances to pair with a positive instance.

  • lr: learning rate 学习速率

  • activation: Activation for ReLU, sigmoid, tanh. 激活函数

  • algorithm: 0 for NAIS_prod, 1 for NAIS_concat ,attention 网络算法选择

定义输入接口

def _create_placeholders(self):
  with tf.name_scope("input_data"):
    self.user_input = tf.placeholder(tf.int32, shape=[None, None]) #the index of users
    self.num_idx = tf.placeholder(tf.float32, shape=[None, 1]) #the number of items rated by users
    self.item_input = tf.placeholder(tf.int32, shape=[None, 1]) #the index of items
    self.labels = tf.placeholder(tf.float32, shape=[None,1]) #the ground truth
  • user_input: 用户的index, shape=[None, None]

  • num_idx: 每个用户评分的物品数量,shape=[None, 1],用来控制衰减参数\beta

  • item_input: 所有物品的index, shape=[None, 1]

  • labels: 标签, shape=[None,1]

创建变量

为了便于理解,简化了参数: batchsize:b embedding size: e weight size: w attention 网络嵌入Q:[N+1, e] 用来训练历史商品的嵌入 Q:[N,e] 用来训练目标商品的嵌入

attention 网络的权重变量

b: [1, w] h:[w,1] dot product algo: W: [e,w] concat product algo: W: [2e, w]

def _create_variables(self):
    with tf.name_scope("embedding"): # The embedding initialization is unknown now
        trainable_flag = (self.pretrain!=2)
        self.c1 = tf.Variable(tf.truncated_normal(shape=[self.num_items, self.embedding_size], \
        mean=0.0, stddev=0.01), \
        name='c1', dtype=tf.float32, trainable=trainable_flag)
        self.c2 = tf.constant(0.0, tf.float32, [1, self.embedding_size], name='c2')
        self.embedding_Q_ = tf.concat([self.c1, self.c2], 0, name='embedding_Q_')
        self.embedding_Q = tf.Variable(tf.truncated_normal(shape=[self.num_items, self.embedding_size],\
        mean=0.0, stddev=0.01), 
        name='embedding_Q', dtype=tf.float32,trainable=trainable_flag)
        self.bias = tf.Variable(tf.zeros(self.num_items),name='bias',trainable=trainable_flag)
        # Variables for attention
        if self.algorithm == 0:
          self.W = tf.Variable(tf.truncated_normal(shape=[self.embedding_size, self.weight_size],
          mean=0.0,
          stddev=tf.sqrt(tf.div(2.0, self.weight_size + self.embedding_size))),\
          name='Weights_for_MLP',dtype=tf.float32, trainable=True)
        else: 
          self.W = tf.Variable(tf.truncated_normal(shape=[2*self.embedding_size,\ 
          self.weight_size],mean=0.0,stddev=tf.sqrt(tf.div(2.0, self.weight_size + \
          (2*self.embedding_size)))),name='Weights_for_MLP',dtype=tf.float32, trainable=True)
          self.b = tf.Variable(tf.truncated_normal(shape=[1, self.weight_size], mean=0.0, \
          stddev=tf.sqrt(tf.div(2.0, self.weight_size + self.embedding_size))),name='Bias_for_MLP',\
          dtype=tf.float32, trainable=True)
          self.h = tf.Variable(tf.ones([self.weight_size, 1]), name='H_for_MLP', dtype=tf.float32)

计算inference

第一步: 在计算输出之前需要先定义两个嵌入矩阵embedding_q_embedding_q分别是从嵌入变量矩阵embedding_Q_embedding_Q中查找出来的,分别对应着用户的历史商品嵌入和目标商品嵌入。

with tf.name_scope("inference"):
    self.embedding_q_ = tf.nn.embedding_lookup(self.embedding_Q_, self.user_input) # (b, n, e)
    self.embedding_q = tf.nn.embedding_lookup(self.embedding_Q, self.item_input) # (b, 1, e)

第二步: 将这两个嵌入矩阵输入到attention网络中,求历史商品的加权和。

if self.algorithm == 0:
    self.embedding_p = self._attention_MLP(self.embedding_q_ * self.embedding_q) #(b,e)
    else:
    n = tf.shape(self.user_input)[1]
    self.embedding_p = self._attention_MLP(tf.concat([self.embedding_q_, \
    tf.tile(self.embedding_q, tf.stack([1,n,1]))],2))

attention network

作者定义了一个attention函数,输入是矩阵q_和q的concat或者点积。输出矩阵每行结果为

 \sum a_{ij}q_j 代码中第一部分求f(p_i,q_j), 第二部分求添加指数衰减的的softmax

def _attention_MLP(self, q_):
    with tf.name_scope("attention_MLP"):
      b = tf.shape(q_)[0]
      n = tf.shape(q_)[1]
      r = (self.algorithm + 1)*self.embedding_size
        
      MLP_output = tf.matmul(tf.reshape(q_,[-1,r]), self.W) + self.b #(b*n, e or 2*e) * \
      (e or 2*e, w) + (1, w)
      if self.activation == 0:
        MLP_output = tf.nn.relu( MLP_output )
      elif self.activation == 1:
        MLP_output = tf.nn.sigmoid( MLP_output )
     elif self.activation == 2:
        MLP_output = tf.nn.tanh( MLP_output )
        
        A_ = tf.reshape(tf.matmul(MLP_output, self.h),[b,n]) #(b*n, w) * \
        (w, 1) => (None, 1) => (b, n)
        
        # softmax for not mask features
        exp_A_ = tf.exp(A_)
        num_idx = tf.reduce_sum(self.num_idx, 1)
        mask_mat = tf.sequence_mask(num_idx, maxlen = n, dtype = tf.float32) # (b, n)
        exp_A_ = mask_mat * exp_A_
        exp_sum = tf.reduce_sum(exp_A_, 1, keep_dims=True) # (b, 1)
        exp_sum = tf.pow(exp_sum, tf.constant(self.beta, tf.float32, [1]))
        
        A = tf.expand_dims(tf.div(exp_A_, exp_sum),2) # (b, n, 1)
        
     return tf.reduce_sum(A * self.embedding_q_, 1)

其中在计算注意力softmax函数的分母的代码有些复杂,其中有个mask的运用tf.sequence_mask( lengths, maxlen=None, dtype=tf.bool, name=None )

tf.sequence_mask([1, 3, 2], 5) # [[True, False, False, False, False],
# [True, True, True, False, False],
# [True, True, False, False, False]]
tf.sequence_mask([[1, 3],[2,0]]) # [[[True, False, False],
# [True, True, True]],
# [[True, True, False],
# [False, False, False]]]

第三步: 求output

self.embedding_q = tf.reduce_sum(self.embedding_q, 1) #(b,e)
self.bias_i = tf.nn.embedding_lookup(self.bias, self.item_input)
self.coeff = tf.pow(self.num_idx, tf.constant(self.alpha, tf.float32, [1]))
self.output = tf.sigmoid(self.coeff *tf.expand_dims(tf.reduce_sum(self.embedding_p*self.embedding_q,\
 1),1)+ self.bias_i)

科普tf.tile(input,multiples,name=None):将input按照维度乘以multiples,其实就是按照维度扩展tf.math.pow(x,y,name=None):指数幂tf.stack(values,axis=0,name='stack'):按axis维度叠加 代码举例:

a = [[[1,1,1]]] #(1,1,3)
b = [[[1,1,1],[1,1,1]]] #(1,2,3)
c = [[[2],[2]]] #(1,2,1)
ta = tf.placeholder(shape=(1,1,3),dtype=tf.float32)
tb = tf.placeholder(shape=(1,2,3),dtype=tf.float32)
tc = tf.placeholder(shape=(1,2,1),dtype=tf.float32)
with tf.Session() as sess:
    print(sess.run(tf.reduce_sum(tb,1), feed_dict={tb:b,tc:c}),'\n')
    print(sess.run(tf.tile(ta, tf.stack([1,2,1])),feed_dict={ta:a,tb:b,tc:c}),'\n')
    print(sess.run(tf.stack([1,2,1]), feed_dict={tb:b,tc:c}),'\n')
    #results
    [[2. 2. 2.]]
    [[[1. 1. 1.]
    [1. 1. 1.]]]
    [1 2 1]


计算Loss

def _create_loss(self):
    with tf.name_scope("loss"):
        self.loss = tf.losses.log_loss(self.labels, self.output) + \
        self.lambda_bilinear * tf.reduce_sum(tf.square(self.embedding_Q)) + \
        self.gamma_bilinear * tf.reduce_sum(tf.square(self.embedding_Q_)) + \
        self.eta_bilinear * tf.reduce_sum(tf.square(self.W))

构建优化器

def _create_optimizer(self):
  with tf.name_scope("optimizer"):
     self.optimizer = tf.train.AdagradOptimizer(learning_rate=self.learning_rate, \
     initial_accumulator_value=1e-8).minimize(self.loss)



本文由 DeepSmart.AI 作者:瑞雪 发表,其版权均为 瑞雪 所有,文章内容系作者个人观点,不代表 DeepSmart.AI 对观点赞同或支持。如需转载,请注明文章来源。本文链接地址:http://www.deepsmart.ai/184.html
avatarDeepSmart

发表评论