LSTM主要增加了门(gate)和核(memorycell)来处理时序问题,cell用来保存"短时记忆",gate控制那部分得以保存,通过sigmoid函数控制(由于gate对信息进行了筛选,因此不会出现梯度消失和梯度爆炸的情况,原始RNN隐藏层的连接则已造成梯度消失),总之,LSTM在更长的序列中比原始的RNN有更好的表现。
如图吴恩达深度学习课程中LSTM结构图:
相比于原始的RNN,LSTM有两个传输状态,一个是 a t a^t at(就是每一个LSTM cell的输出),一个是 c t c^t ct( c t c^t ct其实就相当于LSTM网络对于句子前面的语义的一个短时记忆)
以下为其基本公式:
Γ μ \Gamma_\mu Γμ, Γ f \Gamma_f Γf, Γ o \Gamma_o Γo,分别对应update gate,forget gate和output gate。
从公式和图片可以比较容易的分析出LSTM的基本原理,我们只要记住LSTM有四个输入,一个输出即可。
对于LSTM的讲解以及GRU的知识还可以参照以下博客:
李宏毅版LSTM
李宏毅版GRU
吴恩达版序列模型
下面来说说如何用LSTM进行text embedding(本人的研究方向是跨模态检索)
为了更好的理解text embedding,在此先提一提pytorch中关于变长序列是如何处理的
搬运自Pytorch-tutorials-学习(六)
假设我们有情感分析的例子,对每句话进行一个感情级别的分类,主体流程大概是如下所示:
思路比较简单,但是当我们进行batch个训练数据进行计算的时候,会遇到多个训练样例长度不同的情况,这样我们就会很自然的进行padding,将短句子padding为最长的句子一样。
比如向下图这样:
但是这会有一个问题,什么问题?比如上图,句子”Yes”只有一个单词,但是padding了5个pad符号,这样会导致LSTM对它的表示通过了非常多无用的字符,这样得到的句子表示就会有误差,直观表示如下:
这就引出Pytorch中RNN需要处理变长输入的需要了。在上面这个例子,我们想要得到的表示仅仅是LSTM过完单词”Yes”之后的表示,而不是通过了多个无用的”Pad”得到的表示:如上图。
主要是用函数torch.nn.utils.rnn.pack_padded_sequence()
以及torch.nn.utils.rnn.pad_packed_sequence()
来进行的,分别来看看这两个函数的用法。
这里的pack,理解成压紧比较好。将一个填充过的变长序列压紧。(填充时候,会有冗余,所以压紧一下)
输入的形状可以是(T×B×*).这里T
是最长序列长度,B
是batch_size
, *
代表任意维度(可以是0)。如果batch_first=True
,那么相应的input_size就是(B×T×* )
Variable中保存的序列,应该按序列长度的长短排序,长的在前,短的在后(特别注意需要进行排序
)。
<\br>
即input[:,0]
代表的是最长的序列
packed_padded_sequence
先填充后压紧
参数说明:
input(Variable)——变长序列被填充后的batch
lengths(list[int])——Variable中每个序列的长度
(知道了每个序列的长度,才能知道每个序列处理到多长停止)
batch_first(bool)
返回值:
一个PackedSequence对象,
一个PackedSequence表示如下所示:
具体代码如下:
embed_input_x_packed=pack_padded_sequence(embed_input_x,sentence_lens,batch_first=True)
encoder_outputs_packed,(h_last,c_last)=self.lstm(embed_input_x_packed)
此时返回的h_last
和c_last
就是剔出padding字符后的hidden state和cell state,都是Variable类型的。代表的意思如下(各个句子的表示,lstm只会作用到它实际长度的句子,而不是通过无用的padding字符,下图用红色的打勾来表示):
pad_packed_sequence
先压紧后填充
参数说明:
sequence(PackedSequence)——将要被填充的batch
batch_first(bool)
返回的Varaible的值的size是 T×B×* , T 是最长序列的长度,B 是 batch_size,
如果 batch_first=True,那么返回值是B×T× 。
batch 中的元素将会以它们的长度逆序排列
PackedSequence输入RNN后输出的仍是PackedSequence
之所以要引入pack_padded_sequence是因为序列是变长的,我们在预处理的时候会把序列加pad使其等长,而在训练的时候我们是不希望处理pad.所以之前对变长序列的处理方法是for循环,一个一个放入model中.而现在有了pack_padded_sequence就不需要for了,直接输入一个pack_padded_sequence后的数据,然后输入这个数据的每一条的长度,得到的输出再通过pad_packed_sequence变回原来的形式.
以论文<Look, Imagine and Match: Improving Textual-Visual Cross-Modal Retrieval with Generative Models>源码中的text embedding为例:
# RNN Based Language Model
class EncoderText(nn.Module):
def __init__(self, opt):
super(EncoderText, self).__init__()
self.use_abs = opt.use_abs
self.rnn_type = getattr(opt, 'rnn_type', 'GRU')
#这里的opt是对训练的一些设置参数,等价于opt.rnn_type,默认的网络类型是GRU
if self.rnn_type == 'GRU':
self.num_directions = 1
self.bidirectional = 0
else: #如果网络不是GRU,则设置为Bi-GRU
self.num_directions = 2
self.bidirectional = 1
self.num_layers = opt.num_layers
self.embed_size = opt.embed_size
self.batch_size = opt.batch_size
# word embedding
self.embed = nn.Embedding(opt.vocab_size, opt.word_dim)#源码中词汇表大概~10000,opt.word_dim=300,这里定义词嵌入,输入一个词将会产生300维的词向量。
# caption/text embedding
if 'Bi' in self.rnn_type:
self.rnn = nn.GRU(opt.word_dim, opt.embed_size, opt.num_layers, bias=False, batch_first=True,
bidirectional=self.bidirectional)
self.bi_out = nn.Linear(opt.embed_size * 2, opt.embed_size)
else:
self.rnn = nn.GRU(opt.word_dim, opt.embed_size, opt.num_layers, batch_first=True)
self.init_weights()
def init_weights(self):
self.embed.weight.data.uniform_(-0.1, 0.1)
if 'Bi' in self.rnn_type:
"""Xavier initialization for the fully connected layer"""
r = np.sqrt(6.) / np.sqrt(self.bi_out.in_features + self.bi_out.out_features)
self.bi_out.weight.data.uniform_(-r, r)
self.bi_out.bias.data.fill_(0)
def init_hidden(self, bsz):
weight = next(self.parameters()).data
return Variable(torch.zeros(self.num_layers * self.num_directions , bsz, self.embed_size)).cuda()
def forward_GRU(self, x, lengths):
"""Handles variable size captions"""
# Embed word ids to vectors
#对序列长度数组进行排序, lengths是一个batch_size x 1的矩阵
#sorted_seq_lengths是排序完成的序列长度数组,indices是序列的索引
sorted_seq_lengths, indices = torch.sort(torch.Tensor(list(map(float,lengths))).float(),descending=True)
#deindices是原序列长度数组索引的索引,例如:设原序列长度数组为(13,14,12),则indices=(1,2,0),deindices=(2,0,1)
_, deindices = torch.sort(torch.Tensor(list(map(float,indices))).float(),descending=False)
#对原始序列按照长度进行排序(batch_first=True)
x = x[list(map(int,indices)),:]
x = self.embed(x)
hiddens = self.init_hidden(x.data.size(0))
packed = pack_padded_sequence(x, list(map(int,sorted_seq_lengths)), batch_first=True)
# Forward propagate RNN
out, hiddens = self.rnn(packed,hiddens)
# Reshape *final* output to (batch_size, hidden_size)
padded = pad_packed_sequence(out, batch_first=True)
padded = list(padded)
#恢复排序前的样本顺序
padded[0] = padded[0][list(map(int,deindices)),:]
I = torch.LongTensor(lengths).view(-1, 1, 1)
I = Variable(I.expand(x.size(0), 1, self.embed_size)-1).cuda()
out = torch.gather(padded[0], 1, I).squeeze(1)
# normalization in the joint embedding space
out = l2norm(out)
sent_emb = hiddens.transpose(0,1).contiguous()
sent_emb = sent_emb.view(-1, self.embed_size * self.num_directions)
# take absolute value, used by order embeddings
if self.use_abs:
out = torch.abs(out)
return out