代码地址:https://github.com/coetaur0/ESIM。建议对照着代码来看
论文链接:https://arxiv.org/pdf/1609.06038.pdf
在这里插入图片描述
直接从ESIM模型的forward部分讲起。
Forward函数:
premises、hypotheses:输入的两个句子。Tensor shape为[batch_size,max_length]
preimises_lengths:1d的tensor,表示的是句子未被pandding、截断前的长度。另一个变量同理。
get_mask函数。获取输入句子的掩码。
这里输入的是编码后的句子(batchsize,maxlength)。
首先取batchsize的大小。然后获取输入句子序列里的最大长。
创建一个全1的张量。
假设输入的一个batch的句子序列shape为[batch_size,512]。表示句子同一pandding、截断到512。
这里获取的max_length=256,表示这一个批次的句子里,未padding前,最长的句子有256个token。
那么现在创建一个[batch_size,256]的全1矩阵,即mask矩阵。
最后一句,mask[sequences_batch[:,:max_length]==0]=0.0
sequences_batch[:,:max_length]==0这一句会选择句子序列中前max_length中,所有=0的位置。然后将mask中相同位置的元素修改为0。
举个例子:
序列1: [3, 5, 0, 0],实际长度为2
序列2: [1, 2, 3, 4],实际长度为4
sequences_batch可以表示为:
[[3, 5, 0, 0],
[1, 2, 3, 4]]
sequences_lengths为:
[2
,4]
取max以后,最大值为4.
则最后生成的mask为[[1,1,0,0]
[1,1,1,1]。
词向量嵌入,这里不多说了。
先看相关类描述:
对于序列形式的输入,使用相同的。对于我这种新手来说,还是不清晰,不知道做了什么。
看前向计算部分:
首先要明白 torch.data.new_ones这个函数是干什么的:他返回一个与全1的tensor,其中设备与数据类型与原数据相同,但是返回数据的形状需要指定。(与ones_like不同)。
这里生成的ones的shape为[batch_size,hidden_size]。
然后生成一个dropout张量,nn.functional.dropout会对输入的tensor按照self.p的概率进行dropout。得到的dropout_mask也是[batchsize,hidden_size]。在函数返回的时候,将dropmask与输入的batch个句子相乘。
也就是[batch,max_length,hidden]*[batch,1,hidden]。这里第二个维度会被自动广播。这样的话,对于同一个句子中的不同token中的神经元,采用了相同的dropoutmask。
比如“你好呀”。Shape为[1,3,256]。那么对每一个字[1,256]采用的都是相同的dropoutmask。
回到ESIM模型的前向计算过程中,在实例化了dropout类以后。要对句子进行特征提取。
这里采用一层bi-lstm。输入是嵌入的句子与长度记录tensor[batch,max_length,hidden]、[batch]
接下来看他的encoding是怎么做的。
首先要解决的是sort_by_seq_lens这个函数。函数的功能很明显,看看具体怎么做的。
这里是参数描述。对padding后的序列按照他们未padding前的长度进行排序。
返回变量有4个。
Sorted_batch。就是输入序列排序后的结果。
Sorted_seq_lens。排序后每个句子的长度。就是上面那个返回值每个句子对应的长度。
Sorting_idx:返回每个元素未排序前的索引。比如序列[3,2,1]排序得到[1,2,3],那么返回的sorting_idx就是[2,1,0]表示排序结果中。
Restoration_idx:将序列恢复原始顺序的索引
代码部分,这里首先对sequences_lengths,也就是记录句子长度这个向量进行排序。得到排序结果、以及原来元素排序后的新下标顺序。
之后按照这个索引,对句子序列进行选择,得到的结果就是句子序列排序后的结果。
这里可能有点绕,举个例子。
比如对序列进行排序。
[[1,2,3,0,0],
[1,2,0,0,0],
[1,2,3,4,0,]]
他们对应的未pandding长度tensor为,就是上面的sequences_lengths。
[3,
2,
4]
执行sort函数后得到两个返回值,一个是排序后的结果:
[2,3,4]。另一个是每个元素未排序前的索引[1,0,2]。然后按照这个索引去取句子序列的向量。
结果就是:
[[1,2,0,0,0],
[1,2,3,0,0,],
[1,2,3,4,0]].
是不是相当于对这个序列进行了排序?
这一行是创建了一个从0到len(sequences_lengths)-1的序列。与sequences_lengths保持相同的设备与数据类型。
这一行对sorting_index进行升序排序。用上面的例子,sorting_idx=[1,0,2]按照升序排序返回_=[0,1,2].reverse_mapping=[1,0,2]。最后再对idx range=[0,1,2]进行排序得到restoration_index=[1,0,2]。如果将排序后的序列按照restoration_index进行排序,可以恢复到输入函数时候的顺序。
继续回到利用LSTM做特征抽取这一步。
在对句子进行重排序之后,执行了pack_padded_sequence这一步。该函数用于优化RNN类计算。因为batch中,句子序列各不相同,直接将这些序列填充到相同长度并堆叠在一起会导致计算资源的浪费,尤其是对于较短的序列而言,大量的填充不会被利用。该函数的作用就是移除这些填充,从而让 RNN/LSTM 只对实际的序列数据进行计算,提高效率并减少内存消耗。在前向传播过程中,它将输入序列压缩成一个紧凑的形式,而在反向传播时,需要使用 pad_packed_sequence 函数将输出再展开回原来的形状,以便进行损失计算或其他后续操作。acked_batch 将是一个 PackedSequence 对象,包含了压缩后的序列数据以及关于如何解压的信息。这样,当传递给 RNN 或 LSTM 时,模型将只处理有效的序列部分,忽略填充数据。
这里self._encoder就是普通的单层bi-lstm层。
最后得到outputs就是一个[batch,max_length,2*hidden_size]的输出,然后按照前面说的逆映射将序列的顺序恢复。
在对序列进行特征抽取以后,进行注意力机制。论文中采用向量相乘的方式。
看一下这个attention是怎么做的:
首先是一个矩阵乘法,没什么好说的。、
输入tensor是[batch,max_length,max_length],mask是[batch,max_length] 。然后就是常规的掩码注意力计算。这里额外进行了一次归一化。
经过softmax以后,就是一个权重与向量相乘。
回到ESMI的forward部分
这里这一部分很直观,没什么好说的。
_composition这一部分则又是利用lstm做一次特征计算。然后求最大池化和平均池化。
Replace_masked这部分,是将序列中被mask的部分(=0)的部分用-1e7替代。
最后拼接,进入分类层分类。
总结:从这份代码来看,有几个训练时候的小trick。一个是dropout层的设置。对于RNN类网络,可以对序列中的元素进行统一的dropout。同时,对不同长度序列排序以后,利用pack那个trick,可以提高计算效率,忽略padding部分的计算开销。