LSTM在text embedding中的作用(Cross modal retrieval)

LSTM介绍

 LSTM主要增加了门(gate)和核(memorycell)来处理时序问题,cell用来保存"短时记忆",gate控制那部分得以保存,通过sigmoid函数控制(由于gate对信息进行了筛选,因此不会出现梯度消失和梯度爆炸的情况,原始RNN隐藏层的连接则已造成梯度消失),总之,LSTM在更长的序列中比原始的RNN有更好的表现。
如图吴恩达深度学习课程中LSTM结构图:
LSTM在text embedding中的作用(Cross modal retrieval)_第1张图片
 相比于原始的RNN,LSTM有两个传输状态,一个是 a t a^t at(就是每一个LSTM cell的输出),一个是 c t c^t ct c t c^t ct其实就相当于LSTM网络对于句子前面的语义的一个短时记忆)
以下为其基本公式:
LSTM在text embedding中的作用(Cross modal retrieval)_第2张图片
Γ μ \Gamma_\mu Γμ Γ f \Gamma_f Γf Γ o \Gamma_o Γo,分别对应update gate,forget gate和output gate。

  • Update gate :有的地方也称作输入门,用来控制 x t x^t xt的输入,如果此门关了,即 Γ μ = 0 \Gamma_\mu=0 Γμ=0,则视 x t x^t xt不重要;
  • Forget gate :对上一个节点传进来的输入进行选择性忘记,即是否把 c t c^t ct传递下去。如上公式可以看到输入门(更新门)或者遗忘门有一个以上开着, c t c^t ct都将会更新,从语义上可以理解为,LSTM网络又记忆了一个词的语义,即在下一个cell中考虑了上文环境。
  • Output gate:此门控制着一个LSTM cell的输出,此门若关着,则输出的是 c t − 1 c^{t-1} ct1,从语义上理解,即这个词的语义不重要,只需要前文的语义环境。

从公式和图片可以比较容易的分析出LSTM的基本原理,我们只要记住LSTM有四个输入,一个输出即可。
对于LSTM的讲解以及GRU的知识还可以参照以下博客:
李宏毅版LSTM
李宏毅版GRU
吴恩达版序列模型

下面来说说如何用LSTM进行text embedding(本人的研究方向是跨模态检索)

为了更好的理解text embedding,在此先提一提pytorch中关于变长序列是如何处理的
搬运自Pytorch-tutorials-学习(六)

pytorch如何处理变长序列

为什么RNN需要变长输入

假设我们有情感分析的例子,对每句话进行一个感情级别的分类,主体流程大概是如下所示:
LSTM在text embedding中的作用(Cross modal retrieval)_第3张图片
思路比较简单,但是当我们进行batch个训练数据进行计算的时候,会遇到多个训练样例长度不同的情况,这样我们就会很自然的进行padding,将短句子padding为最长的句子一样

比如向下图这样:
LSTM在text embedding中的作用(Cross modal retrieval)_第4张图片
但是这会有一个问题,什么问题?比如上图,句子”Yes”只有一个单词,但是padding了5个pad符号,这样会导致LSTM对它的表示通过了非常多无用的字符,这样得到的句子表示就会有误差,直观表示如下:
LSTM在text embedding中的作用(Cross modal retrieval)_第5张图片

这就引出Pytorch中RNN需要处理变长输入的需要了。在上面这个例子,我们想要得到的表示仅仅是LSTM过完单词”Yes”之后的表示,而不是通过了多个无用的”Pad”得到的表示:如上图。

pytorch 中RNN如何处理变长padding

主要是用函数torch.nn.utils.rnn.pack_padded_sequence()以及torch.nn.utils.rnn.pad_packed_sequence()来进行的,分别来看看这两个函数的用法。
这里的pack,理解成压紧比较好。将一个填充过的变长序列压紧。(填充时候,会有冗余,所以压紧一下)
输入的形状可以是(T×B×*).这里T是最长序列长度,Bbatch_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表示如下所示:
LSTM在text embedding中的作用(Cross modal retrieval)_第6张图片
具体代码如下:

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_lastc_last就是剔出padding字符后的hidden state和cell state,都是Variable类型的。代表的意思如下(各个句子的表示,lstm只会作用到它实际长度的句子,而不是通过无用的padding字符,下图用红色的打勾来表示):
LSTM在text embedding中的作用(Cross modal retrieval)_第7张图片

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变回原来的形式.

Text Embedding基础代码解析(pytorch)

以论文<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

你可能感兴趣的:(深度学习,pytorch,cross,modal,retrieval)