记忆网络之Dynamic Memory Networks
今天我们要介绍的论文是“Ask Me Anything: Dynamic Memory Networks for Natural Language Processing”,这篇论文发表于2015年6月,从题目中就可以看得出来,本文所提出的模型在多种任务中均取得了非常优秀的表现,论文一开始说道,NLP中很多任务都可以归结为QA问题,所以本文的DMN模型以QA为基础进行训练,但是可以扩展到很多别的任务中,包括序列标注、分类、seq-to-seq、QA等等。之所以有这么强的推广能力,原因就在于其使用RNN作为处理数据的工具,而大部分NLP任务都是序列问题,所以很方便就可以进行推广。而前面介绍的几篇Memory Networks的局限性就在于,其使用词袋子的方法处理输入,使用embedding来编码输入的信息,限制了模型用在别的任务上面。
模型架构
本文提出的DMN网络模型包含输入、问题、情景记忆、回答四个模块,架构图如下所示。模型首先会计算输入和问题的向量表示,然后根据问题触发Attention机制,使用门控的方法选择出跟问题相关的输入。然后情景记忆模块会结合相关的输入和问题进行迭代生成记忆,并且生成一个答案的向量表示。最只答案模块结合该向量以及问题向量,生成最终的答案。从整个框架上看跟MemNN十分相似,也是四大模块,而且各模块的功能都很像。
![](https://i.imgur.com/ZxTtn6D.png)
接下来我们看一个各个模块的细节是如何实现的,这也是其区别与MemNN的地方。首先结合实例和细节模型图了解一下模型的原理:
![](https://i.imgur.com/yKdehTu.png)
如上图所示,假设我们的输入就是Input模块中的8句话,问题是“Where is the football?”首先,模型会将相应的输入都编码成向量表示(使用GRU),如图中的S1~S8和问题向量q。接下来q会触发Attention机制,对输入的向量进行评分计算,如上图中在计算第一层的memory时,只选择了S7(因为问题是足球在哪,而S7是jhon放下了足球,与问题最为相关)。然后q作为第一层GRU的初始隐层状态进行迭代计算得到第一层的记忆m1;之后因为第一层选择出了john这个关键词,所以在第二层memory计算时,q会选择出S2和S6(其中S2属于干扰信息,权重较小),同样结合m1进行迭代计算得到第二层的记忆m2,然后将其作为输出向量传递给Answer模块,最终生成最后的答案hallway。这就是整个模型直观的解释,然后我们在对每个模块一一介绍。
使用GRU对输入进行编码,(这里论文中有提到GRU和LSTM,说GRU可以达到与LSTM相似的准确度而且参数更少计算更为高效,但都比RNN要好),具体GRU的计算公式就不在详细列了,这里说一下当输入不通时应该如何区别对待。
- 输入是一个句子时,直接输入GRU,步长就是句子的长度。最终也会输出句长个向量表示(对应S1~S8),这是Attention机制用来选与Question最相关的单词;
- 输入是一堆句子时,将句子连接成一个长的单词序列,然后每个句子之间使用end-of-sentence的特殊标志进行分割,然后将每个end-of-sentence处的隐层状态输出即可,这时Attention机制选择的就是与Question相关的句子表示;
Question Module
这部分与Input Module一样,就是使用GRU将Question编码成向量。不同的是,最后只输出最后的隐层向量即可,而不需要像Input模块那样(输入是句子时,会输出句长个向量)。而且,q向量除了用于Attention外,还会作为Memory模块GRU的初始隐层状态~~
Episodic Memory Module
本模块主要包含Attention机制、Memory更新机制两部分组成,每次迭代都会通过Attention机制对输入向量进行权重计算,然后生成新的记忆。
首先看一下Attention机制,这里使用一个门控函数作为Attention。输入是本时刻的输入c,前一时刻的记忆m和问题q。首先计算相互之间的相似度作为特征向量传入一个两层的神经网络,最终计算出来的值就是门控函数的值,也就是该输入与问题之间的相似度。如下:
![](https://i.imgur.com/aVfWyEK.png)
其中相似度的特征向量是有c,m,q,c*m,c*q,|c-q|,|c-m|,cWq,cWm contact起来的向量,将其传入一个二层神经网络即可。
接下来看一下记忆更新机制,计算出门控函数的值之后,我们就要根据其大小对记忆进行更新。更新方法就是GRU算出的记忆乘以门控值,再加上原始记忆乘以1-门控值,其实就类似于反向传播中的梯度更新一样。公式如下,更直观:
![](https://i.imgur.com/dlercAY.png)
Answer Module
仍然使用GRU最为本模块的模型,根据memory模块最后的输出向量(将其作为初始隐层状态),然后输入使用的是问题和上一时刻的输出值连接起来(每个时刻都是用q向量)。并使用交叉熵损失函数作为loss进行反向传播训练。
模型扩展
以上就是DMN的模型架构及细节介绍,可以看得出来,其每个模块都使用GRU作为编码/记忆的基础模型,而且模型性能很依赖于Attention机制的效果。所以可以从以上两个方面对DMN进行改进,接下来我们就参考两篇论文来说一下他们对DMN作出的改进:
DMTN
这是论文“Ask Me Even More: Dynamic Memory Tensor Networks (Extended Model)”所提出的改进模型,DMN在用于bAbI数据集时,和MemNN一样,需要对答案所依赖的事实进行监督训练。而本文所提出的模型由于改进了Attention机制,所以可以自己学习到这个点。本文提出的模型叫做Dynamic Memory Tensor Network(DMTN),主要的改进点就是DMN的Attention机制,其他地方未做改动。
原始的Attention机制,首先根据c,m,q三个向量构造一个相似度的特征向量,然后通过一个二层神经网络计算门控值g(见上图)。而DMTN中,作者认为上面那种人工构造特征向量计算相似度的方法存在某种局限,所以推广到让模型自己学习参数构造该特征向量,最后仍适用一个二层神经网络计算门控值g。如下图所示,其实就是将原来的内积、|~|距离替换成使用矩阵参数学习两个向量之间的相似度而已(个人感觉比较牵强==):
![](https://i.imgur.com/XTjOsH1.png)
然后作者有尝试了另外一种方案,令z=(c,m,q),然后使用下面的公式直接进行计算,而不再使用上面的几个变量连接的方法,这样可以减少参数量的同时保持比较高的准确度:
![](https://i.imgur.com/4Tpko8k.png)
DMN+
这是论文“Dynamic Memory Networks for Visual and Textual Question Answering”提出的改进模型。上面的DMTN改动较小,仅仅是对特征向量表示方法的一些尝试。而本篇论文所提出的改进就相对较多,而且还把模型运用到了visual QA的任务上面并取得了较好地成果。其改进点主要集中在对Input module、Attention机制和Memory更新三个地方,是结合了End-to-End MemNN和DMN两个模型的优点作出的改进,可以说是比较完善的解决方案,下面我们看一下改进的点:
1,Input Module的改进:考虑到DMN的强监督性,作者认为是由两点导致的,一是输入模块采用单向GRU,所以只能记住前向的上下文,而无法get到后向上下文的相关信息。二是答案的依据可能距离问题较远,单词水平的GRU无法记忆这么远的信息。所以模型无法自己学习到所以来的句子,而要靠监督学习辅助。因此本文提出了Input Fusion Layer来解决这个问题。
该模型分为两部分,首先将句子采用positional Encoding的方法编码成一个向量(这部分可以参考End-to-End那篇论文,其实就是对句子中每个单词进行位置编码,然后加权求和。这部分主要是解决上面第二个问题,也就是从句子层面编码,然后再输入GRU,以减小文体之间的距离。而且,根据要解决的任务,问题总是于某个句子相关,所以从句子层面进行建模更合理),这部分就是论文所说的sentence reader。这里作者说也尝试了使用GRU或者LSTM进行句子编码,然后回过拟合,而且计算量较大。所以直接使用这种位置编码的方法效果反而会更好。
接下来将所有句子表示输入一个双向GRU模型进行学习,得到其表示,也就是Input Fusion Layer。这部分主要是让句子之间进行信息的交互,学习到前后上下文的信息并进行编码。从而解决上面提出的第一个问题。其结构如下图所示:
![](https://i.imgur.com/BOh9I6E.png)
2,Attention机制的改进:这部分与DMN思想一样,但是细看会发现有一定区别。这里把一开始简单的门控值作为Attention的方法替换成了使用GRU来产生一个中间输出c。而这个c就相当于End-to-End模型中的softmax的评分向量~~(这里可能比较绕,需要好好理解一下)。
首先同样是使用特征向量来构造门控值,如下图所示,但是这里作者有省去了两项含有参数的部分,一方面是为了减少计算量,另一方面作者认为并不需要这些项,简直与上面那篇论文完全相反的思路,这里在特征向量的构造上做了尽可能的简化。
![](https://i.imgur.com/wlfia1W.png)
有了门控值之后就是计算Attention。这里作者提出两种方案,一是soft Attention,也就是直接加权求和,这样做好处是简单,而且在可微的情况下仅选择出一个与答案相关的依赖项。缺点是丢失了位置和顺序关系的信息==BOW就这个缺点。另外一种是Attention based GRU。其实我感觉这里就是DMN的Memory更新部分所采用的模型,及使用门控值g来控制h隐层值的更新。其与标准GRU的区别如下图所示,
![](https://i.imgur.com/uJW6UMI.png)
这样我们就可以对所有的事实通过Attention机制得到一个中间变量c,恩,其实可以看成是一种f的加权求和~~然后基于c在对记忆进行更新。
3,Memory更新机制的改进:DMN中memory的更新采用以q向量为初始隐层状态的GRU进行更新。而这里作者参考End-to-End模型提出了一种ReLU的单层神经网络用于记忆更新。而记忆m就是上层的输出。这样做的好处应该是在于简化模型吧,毕竟Attention哪里已经用了GRU,产生的c相对来说已经拥有了比较精准的信息量(猜测),所以这里的模型就不用很复杂了。这两部分的架构如下图所示:
![](https://i.imgur.com/gKc8MUD.png)
至此我们已经对DMN以及DMN的扩展模型进行了介绍,下面我们结合代码来看一下如何实现~~
TensorFlow 实现
因为在github上面已经有比较好的实现方案,所以直接参考barronalex的代码进行介绍。使用的数据集是之前用过的bAbI,所以也省去了数据集介绍和处理的过程。这里主要关注以下attention_gru_cell.py和dmn_plus.py两个文件,分别是构建Attention based GRU和模型构建两个部分。
attention_gru_cell.py
其实这里主要涉及到的知识点是如何在tensorflow中自定义RNN Cell,以满足自己的需求。按照论文中所提到的模型,作者在生成新的记忆时将原始的GRU中的ui替换成前面计算出来的门控值gi,也就是所谓的Attention based GRU,而tf中默认的GRU是采用的原始定义。所以需要定义自己的RNNCell。
这部分代码如下所示,其实很简单,就是定义一个新的类AttentionGRUCell,让其继承RNNCell,然后接下来重新定义他的call()函数即可,这个函数是RNNCell主要的功能实现。可以看下面的代码注释:
class AttentionGRUCell(RNNCell):
"""Gated Recurrent Unit incoporating attention (cf. https://arxiv.org/abs/1603.01417).
Adapted from https://github.com/tensorflow/tensorflow/blob/master/tensorflow/contrib/rnn/python/ops/core_rnn_cell_impl.py
NOTE: Takes an input of shape: (batch_size, max_time_step, input_dim + 1)
Where an input vector of shape: (batch_size, max_time_step, input_dim)
and scalar attention of shape: (batch_size, max_time_step, 1)
are concatenated along the final axis"""
def __init__(self, num_units, input_size=None, activation=tanh):
if input_size is not None:
logging.warn("%s: The input_size parameter is deprecated.", self)
self._num_units = num_units
self._activation = activation
@property
def state_size(self):
return self._num_units
@property
def output_size(self):
return self._num_units
def __call__(self, inputs, state, scope=None):
"""Attention GRU with nunits cells."""
with vs.variable_scope(scope or "attention_gru_cell"):
with vs.variable_scope("gates"):
if inputs.get_shape()[-1] != self._num_units + 1:
raise ValueError("Input should be passed as word input concatenated with 1D attention on end axis")
inputs, g = array_ops.split(inputs,
num_or_size_splits=[self._num_units,1],
axis=1)
r = _linear([inputs, state], self._num_units, True)
r = sigmoid(r)
with vs.variable_scope("candidate"):
r = r*_linear(state, self._num_units, False)
with vs.variable_scope("input"):
x = _linear(inputs, self._num_units, True)
h_hat = self._activation(r + x)
new_h = (1 - g) * state + g * h_hat
return new_h, new_h
def _linear(args, output_size, bias, bias_start=0.0):
"""Linear map: sum_i(args[i] * W[i]), where W[i] is a variable.
Args:
args: a 2D Tensor or a list of 2D, batch x n, Tensors.
output_size: int, second dimension of W[i].
bias: boolean, whether to add a bias term or not.
bias_start: starting value to initialize the bias; 0 by default.
Returns:
A 2D Tensor with shape [batch x output_size] equal to
sum_i(args[i] * W[i]), where W[i]s are newly created matrices.
Raises:
ValueError: if some of the arguments has unspecified or wrong shape.
"""
if args is None or (nest.is_sequence(args) and not args):
raise ValueError("`args` must be specified")
if not nest.is_sequence(args):
args = [args]
total_arg_size = 0
shapes = [a.get_shape() for a in args]
for shape in shapes:
if shape.ndims != 2:
raise ValueError("linear is expecting 2D arguments: %s" % shapes)
if shape[1].value is None:
raise ValueError("linear expects shape[1] to be provided for shape %s, "
"but saw %s" % (shape, shape[1]))
else:
total_arg_size += shape[1].value
dtype = [a.dtype for a in args][0]
scope = vs.get_variable_scope()
with vs.variable_scope(scope) as outer_scope:
weights = vs.get_variable(
"weights", [total_arg_size, output_size], dtype=dtype)
if len(args) == 1:
res = math_ops.matmul(args[0], weights)
else:
res = math_ops.matmul(array_ops.concat(args, 1), weights)
if not bias:
return res
with vs.variable_scope(outer_scope) as inner_scope:
inner_scope.set_partitioner(None)
biases = vs.get_variable(
"biases", [output_size],
dtype=dtype,
initializer=init_ops.constant_initializer(bias_start, dtype=dtype))
return nn_ops.bias_add(res, biases)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
有了attention_gru_cell之后,我们就可以把它当成是普通的RNNCell一样使用,直接传入dynamic_rnn()函数然后构建模型即可。
dmn_plus.py
这里我们按照上面模型的几个组件分别介绍其代码实现,首先看一下各placeholder的定义以及数据载入部分:
1,数据placeholder占位符的定义:
def add_placeholders(self):
self.question_placeholder = tf.placeholder(tf.int32, shape=(self.config.batch_size, self.max_q_len))
self.input_placeholder = tf.placeholder(tf.int32, shape=(self.config.batch_size, self.max_input_len, self.max_sen_len))
#用于dynamic_rnn模型构建时传入各个数据的长度,不懂得可以具体查一下
self.question_len_placeholder = tf.placeholder(tf.int32, shape=(self.config.batch_size,))
self.input_len_placeholder = tf.placeholder(tf.int32, shape=(self.config.batch_size,))
self.answer_placeholder = tf.placeholder(tf.int64, shape=(self.config.batch_size,))
self.rel_label_placeholder = tf.placeholder(tf.int32, shape=(self.config.batch_size, self.num_supporting_facts))
self.dropout_placeholder = tf.placeholder(tf.float32)
2,input module:
def get_input_representation(self, embeddings):
#通过embedding得到词向量表示
inputs = tf.nn.embedding_lookup(embeddings, self.input_placeholder)
# 用positional Encoding的方法对输入进行编码,将句子中的所有单词的词向量转化为一个向量表示。从四维tensor变成三维tensor
inputs = tf.reduce_sum(inputs * self.encoding, 2)
#将句子向量传入双向GRU进行编码,也就是input fusion layer
forward_gru_cell = tf.contrib.rnn.GRUCell(self.config.hidden_size)
backward_gru_cell = tf.contrib.rnn.GRUCell(self.config.hidden_size)
outputs, _ = tf.nn.bidirectional_dynamic_rnn(
forward_gru_cell,
backward_gru_cell,
inputs,
dtype=np.float32,
sequence_length=self.input_len_placeholder
)
# f<-> = f-> + f<-,将双向GRU的结果相加
fact_vecs = tf.reduce_sum(tf.stack(outputs), axis=0)
# apply dropout
fact_vecs = tf.nn.dropout(fact_vecs, self.dropout_placeholder)
return fact_vecs
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
3,Question layer:
def get_question_representation(self, embeddings):
questions = tf.nn.embedding_lookup(embeddings, self.question_placeholder)
gru_cell = tf.contrib.rnn.GRUCell(self.config.hidden_size)
_, q_vec = tf.nn.dynamic_rnn(gru_cell,
questions,
dtype=np.float32,
sequence_length=self.question_len_placeholder
)
return q_vec
4,Attention layer,这里作者采用了DMN的Attention和episode memory,而未使用DMN+的:
def get_attention(self, q_vec, prev_memory, fact_vec, reuse):
with tf.variable_scope("attention", reuse=reuse):
features = [fact_vec*q_vec,
fact_vec*prev_memory,
tf.abs(fact_vec - q_vec),
tf.abs(fact_vec - prev_memory)]
feature_vec = tf.concat(features, 1)
attention = tf.contrib.layers.fully_connected(feature_vec,
self.config.embed_size,
activation_fn=tf.nn.tanh,
reuse=reuse, scope="fc1")
attention = tf.contrib.layers.fully_connected(attention,
1,
activation_fn=None,
reuse=reuse, scope="fc2")
return attention
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
5,episode memory layer:
def generate_episode(self, memory, q_vec, fact_vecs, hop_index):
attentions = [tf.squeeze(
self.get_attention(q_vec, memory, fv, bool(hop_index) or bool(i)), axis=1)
for i, fv in enumerate(tf.unstack(fact_vecs, axis=1))]
attentions = tf.transpose(tf.stack(attentions))
self.attentions.append(attentions)
attentions = tf.nn.softmax(attentions)
attentions = tf.expand_dims(attentions, axis=-1)
reuse = True if hop_index > 0 else False
gru_inputs = tf.concat([fact_vecs, attentions], 2)
with tf.variable_scope('attention_gru', reuse=reuse):
_, episode = tf.nn.dynamic_rnn(AttentionGRUCell(self.config.hidden_size),
gru_inputs,
dtype=np.float32,
sequence_length=self.input_len_placeholder
)
return episode
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
6,answer layer:
def add_answer_module(self, rnn_output, q_vec):
rnn_output = tf.nn.dropout(rnn_output, self.dropout_placeholder)
output = tf.layers.dense(tf.concat([rnn_output, q_vec], 1),
self.vocab_size,
activation=None)
return output
7,将上面几个模块穿起来构成模型的inference函数:
def inference(self):
"""Performs inference on the DMN model"""
embeddings = tf.Variable(self.word_embedding.astype(np.float32), name="Embedding")
with tf.variable_scope("question", initializer=tf.contrib.layers.xavier_initializer()):
print('==> get question representation')
q_vec = self.get_question_representation(embeddings)
with tf.variable_scope("input", initializer=tf.contrib.layers.xavier_initializer()):
print('==> get input representation')
fact_vecs = self.get_input_representation(embeddings)
self.attentions = []
with tf.variable_scope("memory", initializer=tf.contrib.layers.xavier_initializer()):
print('==> build episodic memory')
prev_memory = q_vec
for i in range(self.config.num_hops):
print('==> generating episode', i)
episode = self.generate_episode(prev_memory, q_vec, fact_vecs, i)
with tf.variable_scope("hop_%d" % i):
prev_memory = tf.layers.dense(tf.concat([prev_memory, episode, q_vec], 1),
self.config.hidden_size,
activation=tf.nn.relu)
output = prev_memory
with tf.variable_scope("answer", initializer=tf.contrib.layers.xavier_initializer()):
output = self.add_answer_module(output, q_vec)
return output
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
至此我们也分析完了DMN、DMN+的代码实现过程。结合代码可以对论文有更加深刻的理解和吸收。这篇博客到这里也就结束了~~