论文调研--NAIS -TKDE2018

论文地址

01-NAIS (item sim)

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. 实验设计

参数设置大多数基本和代码中的默认设置差不多,实现过程中,作者发现tensoflow在输入数据的时候,用户数据的历史商品长度应该是相同的,所以作者设计了一种根据用户来分组的mini-batch方法,也就是一个用户的所有正样本和负样本构成一个mini-batch,这样用户矩阵(b,n)的维度n即为用户的历史商品数量,这样设计有两个好处,一个是不需要填充数据,二是免去了batch-size参数的考虑。

另一个需要注意的是,作者采用的数据是另一篇论文已经预处理好的数据,具体论文地址,这些数据的测试集,每一个目标用户商品对,有99个负样本。

数据集
实验结果
参数分析
  • embedding size


作者解释了在嵌入维度为8时,提出的算法效果要弱于基线算法的原因:
在嵌入维度过小时,非线性模型要比线性模型又更强的表示性。这个我不太理解?

  • weight_size(attention 网络的隐藏层维度)


证明了在attention网络嵌入层维度小的时候基于点乘的算法效果要好于基于concat的算法,用
p_i\odot q_j
来学习
p_i^Tq_j
的效果要更好一些,随着维度逐渐增大,基于concat的算法对
p_i
q_j
的特征表示的丰富逐渐补偿了结构上的不足。

  • 平滑因子\beta

可以看到作者提出这个参数的原因,当参数设置为1时,加入attention网络的算法,效果相当糟糕。作者认为是用户历史商品长度变化太大的原因,当attention网络用于文本或图像时,注意力因子长度十分固定,在计算softmax时不会出现这样的问题。

3. 讨论

  • 实时个性化

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

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

从公式中可以看出,本文设计了两种不同的注意力模型结构,一种是直接将p_iq_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]

创建变量

为了便于理解,简化了参数:
batch_size: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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容