详解seq2seq模型中的attention机制,并利用LuongAttention搭建中英机器翻译!

先从我的理解概括Attention机制:在面对一句话时,人们一般不会整句都看完再去理解其意义(如果这个句子较长)。在seq2seq模型中,一整个句子当作输入会给网络带来困扰,并且很有可能丢失序列中靠前的信息。而在decoder模型的输入时,我们同样利用context替换原本的输入。context则是利用这句话学习得到的权重乘以这句话,得到一个新的context。将contex这句话相拼接,输入网络中。这样做使得seq2seq的效果大大提升。因为输入不仅包含了原本的输入序列,还利用context让网络只注意这句话中的某几个单词,而不是整个句子。最关键的利用编码器(encoder)的信息得到contex替换掉原来的输入,让网络只关心某几个单词。下面开始详细介绍,部分图选自网络,如有侵权,请联系我。

先介绍encoer_decoder架构。seq2seq 就是一个Encoder–Decoder 结构的网络。它的输入是一个序列,通过GRU或者LSTM网络得到一个向量,即encoder_output。这个encoder_output实际上代表了输入序列的信息。输出也是一个序列,通过decoder网络(GRU或者LSTM网)将encoder_output作为输入,得到一个序列。

Encoder 中将一个可变长度的信号序列变为固定长度的向量表达,Decoder 将这个固定长度的向量变成可变长度的目标的信号序列。--简书

模型框架:

假设一下这个输入序列是中文,输出序列是英文。

一般来说,这个是encoder网络采用GRU或者LSTM。以GRU举例,如果输入的维度是[seq_len, batch],那么就得到了[seq_len, batch, hidden_size]的encoder_output。pack_padded_sequence解释在这里:https://mp.csdn.net/postedit/102626466

Encoder代码如下(采用pytorch):

import torch
import torch.nn as nn
import torch.nn.functional as F
class Encoder(nn.Module):
    def __init__(self, input_size, hidden_size, n_layer=1, drop_out=0):
        # input_size是指单词数量,hidden_size为gru的hidden的feature
        super(Encoder, self).__init__()
        self.hidden_size = hidden_size
        self.embeddding = nn.Embedding(input_size, self.hidden_size)
        self.gru = nn.GRU(self.hidden_size, self.hidden_size, bidirectional=True,
                          num_layers=n_layer, dropout=(0 if n_layer == 1 else drop_out))

    def forward(self, input_seq, length, hidden=None):
        # input_seq应该为[seq_len, batch]
        # length为input_seq未补齐时的真实长度排序,最大的在前,list(int)
        # embedd.shape = [seq_len, batch, hidden_size]
        embedd = self.embeddding(input_seq)
        pack = torch.nn.utils.rnn.pack_padded_sequence(embedd, length)
        output, hidden = self.gru(pack, hidden)
        output, _ = torch.nn.utils.rnn.pad_packed_sequence(output)

        # encoder_output的shape=[max(length), batch, hidden_size]
        # hidden_output的shape=[2, batch, hidden_size]
        encoder_output = output[:, :, :self.hidden_size] + output[:, :, self.hidden_size:]

        return encoder_output, hidden

上面的都是最基本,接下来是重点。放出详细图:

看起来很复杂,emmmmm......... 确实比较复杂(这图谁看得懂)。LuongAttention是Luong在论文Effective Approaches to Attention-based Neural Machine Translation中提出的。我认为如果理解了这个attention机制,遍地开花的attention机制你能明白了。让我们来慢慢一步一步理解。

先放公式,不然说我不尊重作者了。

我们来计算context,即attention计算结果。Attention向量计算方法如下:

LuongAttention在多种对齐函数进行了实验,下图为LuongAttention设计的三种对齐函数:

啥是对齐函数?看到这里估计更迷糊了。别急,先不看上面的图,开始解释:接着上面的网络,我们已经有了维度为

[seq_len, batch, hidden_size]的encoder_output,和[2, batch, hidden_size]的encoder_hidden(2是因为是双向单层的GRU)。我们需要得到什么?

需要得到一个权重系数与encoder_output相乘,为了计算context方便,encoder_output维度暂时转换[batch, seq_len, hidden_size]。可以看出,seq_len代表一个batch中的一个句子有几个单词。例如[0, 5, 100]即代表第0个句子中的五个单词。我们需要对这5个单词乘以不同的atten_weights。所以我们需要从encoder_output中得到一个这样的向量[batch, 5]。这个atten_weights代表了网络应该着重看重句子中的那几个单词,而不需要注意全部的单词。这就是注意力模型的精髓。现在回头看LuongAttention,我在这里只解释‘general’,另外两个本质上没有任何区别。

Decoder:

好了,现在我们需要得到atten_weights。大家可以结合开始的图看了,建议拿出纸笔,更好的理解。利用encoder_output和encoder_hidden。在decoder网络的一开始,我们将encoder_hidden传给GRU的decoder_hidden,而不是默认的随机初始化,并且利用sos标志传给GRU网络作为输入,得到了de_gru_output 和 de_gru_hidden。de_gru_output.shape为[seq_len, batch, hidden_size]。由于decoder网络的输入是一个单词接一个单词。所以seq_len=1。

现在所有计算atten_weigths的条件都有了。我们需要encoder_output(这个肯定不能少),还有de_gru_output(很多模型会把这里改成hidden之类的,无关紧要)。这个atten_weigths我们可以假设看作翻译结果的第一个英文单词,这个单词需要看中多少个中文单词。根据LuongAttention的‘general’模型,首先对encoder_output进行一层线性层,然后再将de_gru_output与结果相乘,得到的score求内积。事实上这个内积之后的score就是attn_weigths, 它的维度为[batch, max(length)]。代码如下,暂时只看general就行,其他两个模型得到的维度也是一样的:

class Attn(nn.Module):
    def __init__(self, method, hidden_size):
        super(Attn, self).__init__()
        self.method = method
        self.hidden_size = hidden_size
        if self.method not in ['dot', 'general', 'concat']:
            raise ValueError(self.method, "is not an appropriate attention method.")
        if self.method == 'general':
            self.attn = torch.nn.Linear(self.hidden_size, hidden_size)
        elif self.method == 'concat':
            self.attn = torch.nn.Linear(self.hidden_size * 2, hidden_size)
            self.v = torch.nn.Parameter(torch.FloatTensor(hidden_size))

    def forward(self, decoder_output, encoder_output):
        # 这里的output是指上次decoder的output
        # encoder_output的shape=[max(length), batch, hidden_size]
        # decoder_hidden的shape=[1, batch, hidden_size]
        if self.method == 'general':
            energy = self.general_score(decoder_output, encoder_output)
        elif self.method == 'concat':
            energy = self.concat_score(decoder_output, encoder_output)
        elif self.method == 'dot':
            energy = self.dot_score(decoder_output, encoder_output)
        # energy.shape = [batch, max(length)]
        energy = energy.t()
        # attn_energy.shape = [batch, max(length)]
        attn_energy = F.softmax(energy, dim=-1)
        return attn_energy

    def dot_score(self, decoder_output, encoder_output):
        score = torch.sum(decoder_output * encoder_output, dim=2)
        return score

    def general_score(self, decoder_output, encoder_output):
        attn_general = self.attn(encoder_output)
        score = torch.sum(decoder_output * attn_general, dim=2)
        return score

    def concat_score(self, decoder_output, encoder_output):
        attn_concat = self.attn(
            torch.cat((decoder_output.expand(encoder_output.size(0), -1, -1), encoder_output), dim=2))
        score = torch.sum(self.v * attn_concat, dim=2)
        return score

看到这里你就已经学完了attention机制的一大半了。这个forward返回的结果就是atten_weigths。我们将atten_weigths与encoder_output进行相乘得到context,一些编程小技巧在代码中会展示,我就不一一列举了。得到的context.shape =

[batch, 1, hidden_size]。然后变化维度与decoder的GRU得到的de_gru_output拼接,得到concat_input。然后再经过两层线性层与一层softmax,就得到了最终的输出。这就是LuongAttention机制。我认为所有的Attention机制都在于对atten_weigths的计算方式改进。就比如concat模式,将de_gru_output和encoder_output拼接起来以后经过一层激活函数为tanh的线性层,再与一个矩阵相乘(在我看来就是为了变换维度为了更好的计算)。可能利用hidden或者上一个状态的输出。还有更多的attention机制,但是一通百通,相信大家认真写一遍代码,就能完全理解了attention。

# 这个Attention模型得到context之后并不经过gru,lstm之类的,直接经过两层线性层
class LuongAttenDecoder(nn.Module):
    def __init__(self, input_size, hidden_size, method, n_layer=1, drop_out=0.1):
        super(LuongAttenDecoder, self).__init__()
        self.input_size = input_size
        self.hidden_size = hidden_size
        # output_size = input_size,这两个都是代表总单词数量
        self.output_size = input_size
        self.n_layer = n_layer
        self.embedding = nn.Embedding(self.input_size, self.hidden_size)
        self.dropout_embedd = nn.Dropout(drop_out)
        self.gru = nn.GRU(self.hidden_size, self.hidden_size, num_layers=n_layer, dropout=(0 if n_layer == 1 else drop_out))
        self.concat = nn.Linear(self.hidden_size*2, self.hidden_size)
        self.out = nn.Linear(self.hidden_size, self.output_size)

        self.attn_score = Attn(method, self.hidden_size)

    def forward(self, input_seq, de_gru_hidden, encoder_output):
        # input_seq.shape为[seq_len, batch]
        # de_gru_hidden.shape为[seq_len, batch, hidden_size]
        # encoder_output的shape=[max(length), batch, hidden_size]
        # 由于decoder的输入,所以seq_len=1
        # embedd.shape为[seq_len, batch, hidden_size]
        embedd = self.embedding(input_seq)
        embedd_dropout = self.dropout_embedd(embedd)
        # de_gru_output.shape为[seq_len, batch, hidden_size]
        de_gru_output, de_gru_hidden = self.gru(embedd_dropout, de_gru_hidden)

        # attn_weigths.shape = [batch, max(length)]
        attn_weigths = self.attn_score(de_gru_output, encoder_output)
        # attn_weigths.shape = [batch, 1, max(length)]
        attn_weigths = attn_weigths.unsqueeze(1)
        # 将attn_weights与encoderoutput相乘,得到context
        # context.shape = [batch, 1, hidden_size]
        # 这里得到的shape应该跟embedd接近
        context = torch.bmm(attn_weigths, encoder_output.transpose(0, 1))

        de_gru_output = de_gru_output.squeeze(0)
        context = context.squeeze(1)

        concat_input = torch.cat((de_gru_output, context), dim=1)
        concat_output = self.concat(concat_input)
        # out.shape = [batch, output_size]
        out = self.out(concat_output)
        out = F.softmax(out, dim=1)

        return out, de_gru_hidden

另外,基于上诉模型中英机器翻译代码及数据集(1000W条)链接如下。其中的model模型跟这个完全一致。如果还有什么问题,请在下面留言。谢谢大家的观看。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值