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结构。
损失函数:
结构图
2. 实验设计
参数设置大多数基本和代码中的默认设置差不多,实现过程中,作者发现tensoflow在输入数据的时候,用户数据的历史商品长度应该是相同的,所以作者设计了一种根据用户来分组的mini-batch方法,也就是一个用户的所有正样本和负样本构成一个mini-batch,这样用户矩阵(b,n)的维度n即为用户的历史商品数量,这样设计有两个好处,一个是不需要填充数据,二是免去了batch-size参数的考虑。
另一个需要注意的是,作者采用的数据是另一篇论文已经预处理好的数据,具体论文地址,这些数据的测试集,每一个目标用户商品对,有99个负样本。
数据集
实验结果
参数分析
-
embedding size
作者解释了在嵌入维度为8时,提出的算法效果要弱于基线算法的原因:
在嵌入维度过小时,非线性模型要比线性模型又更强的表示性。这个我不太理解?
-
weight_size(attention 网络的隐藏层维度)
证明了在attention网络嵌入层维度小的时候基于点乘的算法效果要好于基于concat的算法,用
-
平滑因子
可以看到作者提出这个参数的原因,当参数设置为1时,加入attention网络的算法,效果相当糟糕。作者认为是用户历史商品长度变化太大的原因,当attention网络用于文本或图像时,注意力因子长度十分固定,在计算softmax时不会出现这样的问题。
3. 讨论
-
实时个性化
本文设计的算法在支持实时更新用户特征上有着出色的表现。对于实时个性化,需要实时监控用户行为,用户在对某个商品交互后,实施推荐系统同时更新用户的推荐列表。因为重新训练整个模型不现实,一般都选择更新模型参数,然而因为用户行为可能并行发生,更新模型的固有参数会发生冲突,虽然可以通过分布式结构来解决但是分布式往往需要更多的消耗。本文的算法在实时问题上有更好的解决方式,首先用户的特征可以直接通过加法更新,时间消耗基本是常数级的。比如系统检测到用户u消耗了商品t,这时用户u对于商品i的预测为原预测值的基础上加上,根本不需要重新计算模型。
-
两种注意力模型结构的选择
从公式中可以看出,本文设计了两种不同的注意力模型结构,一种是直接将和直接连接在一起,组成不同shape的特征矩阵,另一种则是计算和的点乘。前者保留了商品特征的原始结构,但是因为矩阵的结构发生变化可能导致网络难以收敛。后者的矩阵结构满足学习的目标,但是丢失了学习的商品特征。两种结构各有利弊,也是作者设计两种模型的原因。
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],用来控制衰减参数
- 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或者点积。输出矩阵每行结果为为
代码中第一部分求
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)