一般编码器中得到的向量包含所有的上下文信息, 是高度压缩的信息, 怎么样来避免这种压缩呢?

在这里插入图片描述

  • 这是原来的seq2seq, 当然有些小的改进在此不做介绍了。

主要使用attention

attention 向量a, 长度是源向量的长度, ai在0~1之间, 总和为1, 可以得到加权后的向量w
在这里插入图片描述
在解码的每个时间步计算一个加权向量, 把它作为解码器和线性层的输入来进行预测

Encoder

使用双向GRU构建编码器

在这里插入图片描述

  • 此时output的大小为src_len, batch_size, hid_dim*num_directions , 其中第三轴的hidden的元素是两个分开的, 一个是前向的一个是后向的, 如下图
    在这里插入图片描述
    可以把所有的hidden前向和后向串联表示, 如下图:
    在这里插入图片描述

  • 此时hidden的大小是n_layers*num directions, batch_size, hid_dim
    其中[-2, :, :]给出顶层RNN在最后一个时间步长后的隐藏状态(即在它看到句子中的最后一个单词后),[-1, :, :]给出顶层RNN在最后一个时间步长后的隐藏状态(即在它看到句子中的第一个单词后)。

  • 由于解码器不是双向的,它只需要一个上下文向量z来作为它的初始隐藏状态s0,而我们目前有两个,一个向前的,一个向后的( z → = h → T \overrightarrow z=\overrightarrow h_T z =h T z ← = h ← T \overleftarrow z=\overleftarrow h_T z =h T,分别)。我们通过将两个上下文向量连接在一起,通过一个线性层g,并应用tanh激活函数来解决这个问题。公式如下:
    在这里插入图片描述

class Encoder(nn.Module):
    def __init__(self, input_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout):
        super().__init__()
        
        self.embedding = nn.Embedding(input_dim, emb_dim)
        
        self.rnn = nn.GRU(emb_dim, enc_hid_dim, bidirectional = True)
        
        self.fc = nn.Linear(enc_hid_dim * 2, dec_hid_dim)
        
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, src):
        
        #src = [src len, batch size]
        
        embedded = self.dropout(self.embedding(src))
        
        #embedded = [src len, batch size, emb dim]
        
        outputs, hidden = self.rnn(embedded)
                
        #outputs = [src len, batch size, hid dim * num directions]
        #hidden = [n layers * num directions, batch size, hid dim]
        
        #hidden is stacked [forward_1, backward_1, forward_2, backward_2, ...]
        #outputs are always from the last layer
        
        #hidden [-2, :, : ] is the last of the forwards RNN 
        #hidden [-1, :, : ] is the last of the backwards RNN
        
        #initial decoder hidden is final hidden state of the forwards and backwards 
        #  encoder RNNs fed through a linear layer
        hidden = torch.tanh(self.fc(
        torch.cat((hidden[-2,:,:], hidden[-1,:,:]), dim = 1)))
        
        #outputs = [src len, batch size, enc hid dim * 2]
        #hidden = [batch size, dec hid dim]
        
        return outputs, hidden
Attention

注意力层将解码器先前隐藏状态 st−1 以及来自编码器所有堆叠的前向和后向隐藏状态 H
首先,我们计算之前解码器隐藏状态和编码器隐藏状态之间的energy。由于我们的编码器隐藏状态是一个T序列的张量,而我们之前的解码器隐藏状态是一个单一的张量,我们做的第一件事就是**重复(repeat)**之前的解码器隐藏状态T次。然后我们计算它们之间的能量Et,方法是将它们串联起来,并通过一个线性层(attn)和一个tanh激活函数
在这里插入图片描述
这可以被认为是计算每个编码器隐藏状态"匹配"previous的解码器隐藏状态的程度。目前,我们有一个[dec hid dim, src len]张量,用于批处理中的每个例子。我们希望该批中的每个例子都是[src len],因为注意力应该覆盖在源句的长度上。这是通过将 energy乘以[1, dec hid dim]张量v来实现的。
在这里插入图片描述
我们可以认为v是所有编码器隐藏状态的能量加权和的权重。这些权重告诉我们,我们应该对源序列中的每个符号关注多少。v的参数是随机初始化的,但通过反向传播与模型的其他部分一起学习。请注意v是如何不依赖于时间的,并且在解码的每个时间步中使用相同的v。我们将v实现为一个没有偏置的线性层。

最后,我们确保注意力矢量符合所有元素都在0和1之间的约束条件,并通过softmax层使矢量之和为1。
在这里插入图片描述

从图形上看,这看起来像下面的东西。这是用来计算第一个注意力向量的,其中 s t − 1 = s 0 = z s_{t-1}=s_0=z st1=s0=z。绿色/茶色块代表前向和后向RNN的隐藏状态,而注意力的计算都是在粉色块中完成的。

在这里插入图片描述

class Attention(nn.Module):
    def __init__(self, enc_hid_dim, dec_hid_dim):
        super().__init__()
        
        self.attn = nn.Linear((enc_hid_dim * 2) + dec_hid_dim, dec_hid_dim)
        self.v = nn.Linear(dec_hid_dim, 1, bias = False)
        
    def forward(self, hidden, encoder_outputs):
        
        #hidden = [batch size, dec hid dim]
        #encoder_outputs = [src len, batch size, enc hid dim * 2]
        
        batch_size = encoder_outputs.shape[1]
        src_len = encoder_outputs.shape[0]
        
        #repeat decoder hidden state src_len times
        hidden = hidden.unsqueeze(1).repeat(1, src_len, 1)
        
        encoder_outputs = encoder_outputs.permute(1, 0, 2)
        
        #hidden = [batch size, src len, dec hid dim]
        #encoder_outputs = [batch size, src len, enc hid dim * 2]
        
        energy = torch.tanh(self.attn(torch.cat((hidden, encoder_outputs), dim = 2))) 
        
        #energy = [batch size, src len, dec hid dim]

        attention = self.v(energy).squeeze(2)
        
        #attention= [batch size, src len]
        
        return F.softmax(attention, dim=1)
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐