欢迎关注公众号:DeepL Newer

MENU

Seq2Seq(Attention)的PyTorch实现

July 2, 2020 • Read: 17750 • Deep Learning阅读设置

B站视频讲解

文本主要介绍一下如何使用PyTorch复现Seq2Seq(with Attention),实现简单的机器翻译任务,请先阅读论文Neural Machine Translation by Jointly Learning to Align and Translate,之后花上15分钟阅读我的这两篇文章Seq2Seq 与注意力机制图解Attention,最后再来看文本,方能达到醍醐灌顶,事半功倍的效果

数据预处理

数据预处理的代码其实就是调用各种API,我不希望读者被这些不太重要的部分分散了注意力,因此这里我不贴代码,仅口述一下带过即可

如下图所示,本文使用的是德语→英语数据集,输入是德语,并且输入的每个句子开头和结尾都带有特殊的标识符。输出是英语,并且输出的每个句子开头和结尾也都带有特殊标识符

不管是英语还是德语,每句话长度都是不固定的,所以我对于每个batch内的句子,将它们的长度通过加<PAD>变得一样,也就说,一个batch内的句子,长度都是相同的,不同batch内的句子长度不一定相同。下图维度表示分别是[seq_len, batch_size]

随便打印一条数据,看一下数据封装的形式

在数据预处理的时候,需要将源句子和目标句子分开构建字典,也就是单独对德语构建一个词库,对英语构建一个词库

Encoder

Encoder我是用的单层双向GRU

双向GRU的隐藏状态输出由两个向量拼接而成,例如$h_1=[\overrightarrow{h_1};\overleftarrow{h_T}]$,$h_2=[\overrightarrow{h_2};\overleftarrow{h}_{T-1}]$......所有时刻的最后一层隐藏状态就构成了GRU的output

$$ output=\{h_1,h_2,...h_T\} $$

假设这是个m层GRU,那么最后一个时刻所有层中的隐藏状态就构成了GRU的final hidden states

$$ hidden=\{h^1_T,h^2_T,...,h^m_T\} $$

其中

$$ h^i_T=[\overrightarrow{h^i_T};\overleftarrow{h^i_1}] $$

所以

$$ hidden=\{[\overrightarrow{h^1_T};\overleftarrow{h^1_1}],[\overrightarrow{h^2_T};\overleftarrow{h^2_1}],...,[\overrightarrow{h^m_T};\overleftarrow{h^m_1}]\} $$

根据论文,或者你看了我的图解Attention这篇文章就会知道,我们需要的是hidden的最后一层输出(包括正向和反向),因此我们可以通过hidden[-2,:,:]hidden[-1,:,:]取出最后一层的hidden states,将它们拼接起来记作$s_0$

最后一个细节之处在于,$s_0$的维度是[batch_size, en_hid_dim*2],即便是没有Attention机制,将$s_0$作为Decoder的初始隐藏状态也不对,因为维度不匹配,Decoder的初始隐藏状态是三维的,而现在我们的$s_0$是二维的,因此需要将$s_0$的维度转为三维,并且还要调整各个维度上的值。首先我通过一个全连接神经网络,将$s_0$的维度变为[batch_size, dec_hid_dim]

Encoder的细节就这么多,下面直接上代码,我的代码风格是,注释在上,代码在下

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]
        '''
        src = src.transpose(0, 1) # src = [batch_size, src_len]
        embedded = self.dropout(self.embedding(src)).transpose(0, 1) # embedded = [src_len, batch_size, emb_dim]
        
        # enc_output = [src_len, batch_size, hid_dim * num_directions]
        # enc_hidden = [n_layers * num_directions, batch_size, hid_dim]
        enc_output, enc_hidden = self.rnn(embedded) # if h_0 is not give, it will be set 0 acquiescently

        # enc_hidden is stacked [forward_1, backward_1, forward_2, backward_2, ...]
        # enc_output are always from the last layer
        
        # enc_hidden [-2, :, : ] is the last of the forwards RNN 
        # enc_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
        # s = [batch_size, dec_hid_dim]
        s = torch.tanh(self.fc(torch.cat((enc_hidden[-2,:,:], enc_hidden[-1,:,:]), dim = 1)))
        
        return enc_output, s

Attention

attention无非就是三个公式

$$ E_t=tanh(attn(s_{t-1},H))\\ \tilde{a_t}=vE_t\\ {a_t}=softmax(\tilde{a_t}) $$

其中$s_{t-1}$指的就是Encoder中的变量s,$H$指的就是Encoder中的变量enc_output,$attn()$其实就是一个简单的全连接神经网络

我们可以从最后一个公式反推各个变量的维度是什么,或者维度有什么要求

首先$a_t$的维度应该是[batch_size, src_len],这是毋庸置疑的,那么$\tilde{a_t}$的维度也应该是[batch_size, src_len],或者$\tilde{a_t}$是个三维的,但是某个维度值为1,可以通过squeeze()变成两维的。这里我们先假设$\tilde{a_t}$的维度是[batch_size, src_len, 1],等会儿我再解释为什么要这样假设

继续往上推,变量$v$的维度就应该是[?, 1]?表示我暂时不知道它的值应该是多少。$E_t$的维度应该是[batch_size, src_len, ?]

现在已知$H$的维度是[batch_size, src_len, enc_hid_dim*2],$s_{t-1}$目前的维度是[batch_size, dec_hid_dim],这两个变量需要做拼接,送入全连接神经网络,因此我们首先需要将$s_{t-1}$的维度变成[batch_size, src_len, dec_hid_dim],拼接之后的维度就变成[batch_size, src_len, enc_hid_dim*2+dec_hid_dim],于是$attn()$这个函数的输入输出值也就有了

attn = nn.Linear(enc_hid_dim*2+dec_hid_dim, ?)

到此为止,除了?部分的值不清楚,其它所有维度都推导出来了。现在我们回过头思考一下?设置成多少,好像其实并没有任何限制,所以我们可以设置?为任何值(在代码中我设置?dec_hid_dim

Attention细节就这么多,下面给出代码

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, bias=False)
        self.v = nn.Linear(dec_hid_dim, 1, bias = False)
        
    def forward(self, s, enc_output):
        
        # s = [batch_size, dec_hid_dim]
        # enc_output = [src_len, batch_size, enc_hid_dim * 2]
        
        batch_size = enc_output.shape[1]
        src_len = enc_output.shape[0]
        
        # repeat decoder hidden state src_len times
        # s = [batch_size, src_len, dec_hid_dim]
        # enc_output = [batch_size, src_len, enc_hid_dim * 2]
        s = s.unsqueeze(1).repeat(1, src_len, 1)
        enc_output = enc_output.transpose(0, 1)
        
        # energy = [batch_size, src_len, dec_hid_dim]
        energy = torch.tanh(self.attn(torch.cat((s, enc_output), dim = 2)))
        
        # attention = [batch_size, src_len]
        attention = self.v(energy).squeeze(2)
        
        return F.softmax(attention, dim=1)

Seq2Seq(with Attention)

我调换一下顺序,先讲Seq2Seq,再讲Decoder的部分

传统Seq2Seq是直接将句子中每个词连续不断输入Decoder进行训练,而引入Attention机制之后,我需要能够人为控制一个词一个词进行输入(因为输入每个词到Decoder,需要再做一些运算),所以在代码中会看到我使用了for循环,循环trg_len-1次(开头的<SOS>我手动输入,所以循环少一次)

并且训练过程中我使用了一种叫做Teacher Forcing的机制,保证训练速度的同时增加鲁棒性,如果不了解Teacher Forcing可以看我的这篇文章

思考一下for循环中应该要做哪些事?首先要将变量传入Decoder,由于Attention的计算是在Decoder的内部进行的,所以我需要将dec_inputsenc_output这三个变量传入Decoder,Decoder会返回dec_output以及新的s。之后根据概率对dec_output做Teacher Forcing即可

Seq2Seq细节就这么多,下面给出代码

class Seq2Seq(nn.Module):
    def __init__(self, encoder, decoder, device):
        super().__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.device = device
        
    def forward(self, src, trg, teacher_forcing_ratio = 0.5):
        
        # src = [src_len, batch_size]
        # trg = [trg_len, batch_size]
        # teacher_forcing_ratio is probability to use teacher forcing
        
        batch_size = src.shape[1]
        trg_len = trg.shape[0]
        trg_vocab_size = self.decoder.output_dim
        
        # tensor to store decoder outputs
        outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
        
        # enc_output is all hidden states of the input sequence, back and forwards
        # s is the final forward and backward hidden states, passed through a linear layer
        enc_output, s = self.encoder(src)
                
        # first input to the decoder is the <sos> tokens
        dec_input = trg[0,:]
        
        for t in range(1, trg_len):
            
            # insert dec_input token embedding, previous hidden state and all encoder hidden states
            # receive output tensor (predictions) and new hidden state
            dec_output, s = self.decoder(dec_input, s, enc_output)
            
            # place predictions in a tensor holding predictions for each token
            outputs[t] = dec_output
            
            # decide if we are going to use teacher forcing or not
            teacher_force = random.random() < teacher_forcing_ratio
            
            # get the highest predicted token from our predictions
            top1 = dec_output.argmax(1) 
            
            # if teacher forcing, use actual next token as next input
            # if not, use predicted token
            dec_input = trg[t] if teacher_force else top1

        return outputs

Decoder

Decoder我用的是单向单层GRU

Decoder部分实际上也就是三个公式

$$ c=a_tH\\ s_t=GRU(emb(y_t), c, s_{t-1})\\ \hat{y_t}=f(emb(y_t), c, s_t) $$

$H$指的是Encoder中的变量enc_output,$emb(y_t)$指的是将dec_input经过WordEmbedding后得到的结果,$f()$函数实际上就是为了转换维度,因为需要的输出是TRG_VOCAB_SIZE大小。其中有个细节,GRU的参数只有两个,一个输入,一个隐藏层输入,但是上面的公式有三个变量,所以我们应该选一个作为隐藏层输入,另外两个"整合"一下,作为输入

我们从第一个公式正推各个变量的维度是什么

首先在Encoder中最开始先调用一次Attention,得到权重$a_t$,它的维度是[batch_size, src_len],而$H$的维度是[src_len, batch_size, enc_hid_dim*2],它俩要相乘,同时应该保留batch_size这个维度,所以应该先将$a_t$扩展一维,然后调换一下$H$维度的顺序,之后再按照batch相乘(即同一个batch内的矩阵相乘)

a = a.unsqueeze(1) # [batch_size, 1, src_len]
H = H.transpose(0, 1) # [batch_size, src_len, enc_hid_dim*2]
c = torch.bmm(a, h) # [batch_size, 1, enc_hid_dim*2]

前面也说了,由于GRU不需要三个变量,所以需要将$emb(y_t)$和$c$整合一下,$y_t$实际上就是Seq2Seq类中的dec_input变量,它的维度是[batch_size],因此先将$y_t$扩展一个维度,再通过WordEmbedding,这样他就变成[batch_size, 1, emb_dim]。最后对$c$和$emb(y_t)$进行concat

y = y.unsqueeze(1) # [batch_size, 1]
emb_y = self.emb(y) # [batch_size, 1, emb_dim]
rnn_input = torch.cat((emb_y, c), dim=2) # [batch_size, 1, emb_dim+enc_hid_dim*2]

$s_{t-1}$的维度是[batch_size, dec_hid_dim],所以应该先将其拓展一个维度

rnn_input = rnn_input.transpose(0, 1) # [1, batch_size, emb_dim+enc_hid_dim*2]
s = s.unsqueeze(1) # [batch_size, 1, dec_hid_dim]

# dec_output = [1, batch_size, dec_hid_dim]
# dec_hidden = [1, batch_size, dec_hid_dim] = s (new, is not s previously)
dec_output, dec_hidden = self.rnn(rnn_input, s)

最后一个公式,需要将三个变量全部拼接在一起,然后通过一个全连接神经网络,得到最终的预测。我们先分析下这个三个变量的维度,$emb(y_t)$的维度是[batch_size, 1, emb_dim],$c$的维度是[batch_size, 1, enc_hid_dim*2],$s_t$的维度是[1, batch_size, dec_hid_dim],因此我们可以像下面这样把他们全部拼接起来

emd_y = emb_y.squeeze(1) # [batch_size, emb_dim]
c = w.squeeze(1) # [batch_size, enc_hid_dim*2]
s = s.squeeze(0) # [batch_size, dec_hid_dim]

fc_input = torch.cat((emb_y, c, s), dim=1) # [batch_size, enc_hid_dim*2+dec_hid_dim+emb_hid] 

以上就是Decoder部分的细节,下面给出代码(上面的那些只是示例代码,和下面代码变量名可能不一样)

class Decoder(nn.Module):
    def __init__(self, output_dim, emb_dim, enc_hid_dim, dec_hid_dim, dropout, attention):
        super().__init__()
        self.output_dim = output_dim
        self.attention = attention
        self.embedding = nn.Embedding(output_dim, emb_dim)
        self.rnn = nn.GRU((enc_hid_dim * 2) + emb_dim, dec_hid_dim)
        self.fc_out = nn.Linear((enc_hid_dim * 2) + dec_hid_dim + emb_dim, output_dim)
        self.dropout = nn.Dropout(dropout)
        
    def forward(self, dec_input, s, enc_output):
             
        # dec_input = [batch_size]
        # s = [batch_size, dec_hid_dim]
        # enc_output = [src_len, batch_size, enc_hid_dim * 2]
        
        dec_input = dec_input.unsqueeze(1) # dec_input = [batch_size, 1]
        
        embedded = self.dropout(self.embedding(dec_input)).transpose(0, 1) # embedded = [1, batch_size, emb_dim]
        
        # a = [batch_size, 1, src_len]  
        a = self.attention(s, enc_output).unsqueeze(1)
        
        # enc_output = [batch_size, src_len, enc_hid_dim * 2]
        enc_output = enc_output.transpose(0, 1)

        # c = [1, batch_size, enc_hid_dim * 2]
        c = torch.bmm(a, enc_output).transpose(0, 1)

        # rnn_input = [1, batch_size, (enc_hid_dim * 2) + emb_dim]
        rnn_input = torch.cat((embedded, c), dim = 2)
            
        # dec_output = [src_len(=1), batch_size, dec_hid_dim]
        # dec_hidden = [n_layers * num_directions, batch_size, dec_hid_dim]
        dec_output, dec_hidden = self.rnn(rnn_input, s.unsqueeze(0))
        
        # embedded = [batch_size, emb_dim]
        # dec_output = [batch_size, dec_hid_dim]
        # c = [batch_size, enc_hid_dim * 2]
        embedded = embedded.squeeze(0)
        dec_output = dec_output.squeeze(0)
        c = c.squeeze(0)
        
        # pred = [batch_size, output_dim]
        pred = self.fc_out(torch.cat((dec_output, c, embedded), dim = 1))
        
        return pred, dec_hidden.squeeze(0)

定义模型

INPUT_DIM = len(SRC.vocab)
OUTPUT_DIM = len(TRG.vocab)
ENC_EMB_DIM = 256
DEC_EMB_DIM = 256
ENC_HID_DIM = 512
DEC_HID_DIM = 512
ENC_DROPOUT = 0.5
DEC_DROPOUT = 0.5

attn = Attention(ENC_HID_DIM, DEC_HID_DIM)
enc = Encoder(INPUT_DIM, ENC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, ENC_DROPOUT)
dec = Decoder(OUTPUT_DIM, DEC_EMB_DIM, ENC_HID_DIM, DEC_HID_DIM, DEC_DROPOUT, attn)

model = Seq2Seq(enc, dec, device).to(device)
TRG_PAD_IDX = TRG.vocab.stoi[TRG.pad_token]
criterion = nn.CrossEntropyLoss(ignore_index = TRG_PAD_IDX).to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)

倒数第二行CrossEntropyLoss()中的参数很少见,ignore_index=TRG_PAD_IDX,这个参数的作用是忽略某一类别,不计算其loss,但是要注意,忽略的是真实值中的类别,例如下面的代码,真实值的类别都是1,而预测值全部预测认为是2(下标从0开始),同时loss function设置忽略第一类的loss,此时会打印出0

label = torch.tensor([1, 1, 1])
pred = torch.tensor([[0.1, 0.2, 0.6], [0.2, 0.1, 0.8], [0.1, 0.1, 0.9]])
loss_fn = nn.CrossEntropyLoss(ignore_index=1)
print(loss_fn(pred, label).item()) # 0

如果设置loss function忽略第二类,此时loss并不会为0

label = torch.tensor([1, 1, 1])
pred = torch.tensor([[0.1, 0.2, 0.6], [0.2, 0.1, 0.8], [0.1, 0.1, 0.9]])
loss_fn = nn.CrossEntropyLoss(ignore_index=2)
print(loss_fn(pred, label).item()) # 1.359844

最后给出完整代码链接(需要科学的力量)
Github项目地址:nlp-tutorial

Last Modified: September 20, 2021
Archives Tip
QR Code for this page
Tipping QR Code
Leave a Comment

108 Comments
  1. ChanggengWei ChanggengWei

    楼主这么细致的讲解,这种认真的态度真是让人感动,不得不点赞。
    不过我有一点不太明白,在Encoder中“将 s0作为 Decoder 的初始隐藏状态也不对,因为维度不匹配,需要将s0的维度转为[batch_size, src_len, dec_hid_dim]”不晓得是否有误
    根据pytorch文档,RNN初始h0的维度为(num_layers * num_directions, batch, hidden_size),其中并未有src_len,本人初学,还请指教@(呵呵)

    1. mathor mathor

      @ChanggengWei我这里确实是写错了,我本意是想说”先转成三维,然后再调整各个维度具体的值”
      你说的是对的,我明天早上起来改一下,感谢

    2. ChanggengWei ChanggengWei

      @mathor另外,在Attention,forward模块中, energy = torch.tanh(self.attn(torch.cat((s, enc_output), dim = 2)))上方的维度注释是否应为# enc_output = [batch_size, src_len, enc_hid_dim * 2]?

    3. mathor mathor

      @ChanggengWei不,因为我在cat完之后,还通过了一个全连接,即self.attn,将维度转为了dec_hid_dim,而不是乘以2

  2. 滑天下之大j 滑天下之大j

    博主你好,请问attention中求s和H匹配度的算法您有研究吗?我看您是将s扩维和H连接后做tanh,而原论文中将s和H各乘一个矩阵后相加再求tanh,请问这个对算法性能有什么影响?谢谢!

    1. mathor mathor

      @滑天下之大j没有研究,说实在的,这个影响我也不知道,我个人觉得其实都大差不差

    2. 滑天下之大j 滑天下之大j

      @mathor好吧,谢谢回复

  3. ChanggengWei ChanggengWei

    你好,有个问题还想请教一下,在Attention的第一个公式中,Et由前一时刻的st-1与encoder的所有enc_ouput(即H)做一个全连接attn再一个非线性激活tanh得到,看了原论文,也确实是这样。但我有一点疑问,由这个公式得到的Et仅包含Decoder此时的位置(或者说时刻)信息,并没有包含Decoder的输入信息,似乎并不能表征Dcoder当前输入与Encoder所有位置enc_output的关系,这样算得到的权重at似乎有点问题,并不能表征当前输入对Encoder中enc_input的关注度,不知我的理解是否有误

    1. mathor mathor

      @ChanggengWei那也没办法,毕竟Decoder刚要开始计算时(即输入<s>时),Encoder就得把相关信息传给Decoder了,所以Encoder刚开始计算注意力的时候,确实没有Decoder当前输入对Encoder的信息

      刚开始的时候计算的注意力,其实可以看作是Encoder内部做的一次"self-attention"

      等Decoder第二个时刻进行输入时,计算的attention才同时包含Encoder和Decoder的信息

  4. calrk calrk

    问一个问题啊,attention里面的S_{t-1}是Enconder的隐状态还是Decoder的隐状态啊

    1. mathor mathor

      @calrks_0是Encoder产生的。除s_0以外,都是Decoder产生的

  5. w w

    您好,我想问一下关于seq2seq代码中,初始化的outputs是全0的矩阵,t从1开始分别将t时刻的预测结果存入outputs,但是不是缺少了t=0时刻的预测呢,就是每一个batch中解码器的<sos>的输入对应的预测

  6. vie vie

    完整代码需要访问权限

    1. mathor mathor

      @vie您可以直接clone这个仓库到本地,然后打开文件查看
      我目前还不知道如何取消访问权限

    2. vie vie

      @mathor好的,我也发现了 github 上有代码。

    3. Anna Anna

      @mathor是不是可以更改查看者来修改权限

    4. mathor mathor

      @Anna我已经更新权限了,现在应该可以直接访问了

    5. Anna Anna

      @mathor感谢!

  7. vie vie

    hidden[-2,:,:] 是前向最后时刻的 hidden state
    hidden[-1,:,:] 是反向最后时刻(所有单词结束后),还是第一时刻的呢(只考虑了一个单词)?

  8. wang lu wang lu

    Decoder中
    pred = self.fc_out(torch.cat((dec_output, c, embedded), dim = 1)) ,其中的dec_output是否该改为dec_hidden,因为decoder最后的输出是由s决定的吧?

    1. mathor mathor

      @wang lu没太理解您说要改的原因,不过我觉得在这个代码中,dec_output和dec_hidden的维度都是一样的,所以您将两个换一下位置,应该是跑的通的

      至于您提到的将dec_output改为dec_hidden的原因,我没太理解

    2. baiduzhe baiduzhe

      @mathor在decoder的第3个公式,对3个变量进行串联时的s是dec_hidden吧,但在代码实现中,实际进行串联的却是dec_output,这里是不是有问题呢?

    3. mathor mathor

      @baiduzhe你仔细看下hidden和output的维度,当rnn是单层单向的时候,使用hidden还是output都是一样的,它们的值、维度都相同

    4. learning_NLP learning_NLP

      @wang lu我也觉得是,这里在公式中写的就是Yt = f(emb(yt),c,St),但是代码中却写的是pred = self.fc_out(torch.cat((dec_output,c,embedded),dim=1)),所以我也觉得这里应该将dec_output改成dec_hidden,但是作者这个Decoder用的是单层的GRU,因此dec_output与dec_hidden相同的,但是我觉得这里还是改了好

  9. wworz wworz

    赞赞赞!博主能讲讲pytorch里处理不等长的句子,packed,padded,attention mask机制吗?嘿@(呵呵)@(哈哈)

    1. mathor mathor

      @wworz在我别的文章中讲过了,比方说attention mask在我Transformer里面详细阐述过

  10. 呀是Anna 呀是Anna

    啊。。。,想用这个做工业数据预测上面,在word_embedding那搞不清楚了,想问问要是想用于工业数据预测该怎么改呢T^T

    1. mathor mathor

      @呀是Anna不管什么数据,只要处理成我文章中写的样子就行了

    2. 呀是Anna 呀是Anna

      @mathor我对模型定义里的self.embedding有点搞不清楚,看不明白把单词处理成什么样了T^T

    3. mathor mathor

      @呀是Anna关于word2vec,建议您看下我的这个视频https://www.bilibili.com/video/BV14z4y19777
      其它不懂的地方,例如最基本的seq2seq,也可以看下相关视频

    4. 呀是Anna 呀是Anna

      @mathor好的,谢谢啦

  11. hehe hehe

    你好,您文章中attention的第一个公式中的H指的就是 Encoder 中 的enc_output,但是根据https://nbviewer.jupyter.org/github/bentrevett/pytorch-seq2seq/blob/master/3%20-%20Neural%20Machine%20Translation%20by%20Jointly%20Learning%20to%20Align%20and%20Translate.ipynb
    中H指的是all of the stacked forward and backward hidden states from the encoder

    1. mathor mathor

      @hehe是一样的,enc_output保存的是所有时间戳上的最后一个memory状态,您从维度就可以看出来了,最后的memory状态,是包括前向和反向的隐藏状态的

    2. hehe hehe

      @mathor谢谢,还有个问题,你文章中的“现在已知H的维度是 [batch_size, src_len, enc_hid_dim2], 目前S(t-1)的维度是 [batch_size, dec_hid_dim],这两个变量需要做拼接,送入全连接神经网络,因此我们首先需要将S(t-1) 的维度变成 [batch_size, src_len, dec_hid_dim],拼接之后的维度就变成 [batch_size, src_len, enc_hid_dim2+enc_hid_dim]”拼接后的维度应该是[batch_size, src_len, enc_hid_dim*2+dec_hid_dim]

    3. mathor mathor

      @hehe感谢指出错误,已修改,谢谢

  12. 略不略 略不略

    楼主能提供一下完整版代码的其他链接嘛,需要访问权限T^T

    1. mathor mathor

      @略不略您可以直接在github中download

    2. mathor mathor

      @略不略我已经更新权限了,现在应该可以直接访问了

    3. 略不略 略不略

      @mathor好的,谢谢博主~

  13. Anna Anna

    想向楼主请教在做多变量时序预测的时候好像并不需要用到word embedding,请问这种情况改怎么处理呢,是将程序中的word embedding部分删去嘛,可是感觉删去之后各个维度又不一样了T^T

    1. mathor mathor

      @Annaembedding以后,向量的维度是[seq_len, batch_size, emb_dim]
      对于时间序列预测,不做embedding,只要保证输入的维度还是个三维的就行
      比方说此时的seq_len,可以理解为你数据当中,时间的长度(或者理解为这一段时间内的数据个数),batch_size还是一样的,emb_dim可以理解为你数据当中,每个时刻的特征个数,假如就1个特征,那就是1

    2. Anna Anna

      @mathor恩恩,我自己做单独的lstm时把数据转换成三维的了,那意思是不是就不需要用embedding了啊?
      我有尝试做embedding,但是在训练的时候程序报错说我的索引超过了本身的范围,我在想是不是pad那块的问题啊,我在想对于时序预测有没有必要做embedding

    3. mathor mathor

      @Anna时间序列不需要做embedding,也不能做

    4. Anna Anna

      @mathor哦哦,明白了,谢谢啊

    5. 呀是Anna 呀是Anna

      @mathor想问问x楼主的隐含层每层节点个数是相同的嘛,请问阔以改成不相同的嘛,是修改哪个参数还是在模型结构上修改呀

    6. mathor mathor

      @呀是Anna整体框架用的是两个GRU,具体如何修改,能否修改,请查看torch api

    7. 呀是Anna 呀是Anna

      @mathor哦哦

  14. sd sd

    写得好

  15. woooo woooo

    博主你好,请问下Encoder那块 双向GRU输出的hidden,也就是h{T,i}=h{T,i,->}+h{0,i,<-}这个式子的h{0,i,<-}是否应该是h{1,i,<-},h0不是输入的隐藏状态吗,h{1,i,<-}才是模型最后一层的输出的隐藏状态把,还是说gru最后一层隐藏状态其实就包括了输入的h0,那另外一个隐藏状态不应该是图中的z吗,不是很懂gru,就是感觉这一块没看明白@(笑尿)

    1. mathor mathor

      @woooo我写的确实有点问题,已修改
      我需要的是第一个词输入的时候,这个时刻,rnn输出的隐藏状态,所以应该是h_1,而不是h_0
      h_0是第一个词输入的时候,rnn输入的隐藏状态

    2. woooo woooo

      @mathor好的,谢谢博主的解答了@(哈哈)

    3. mathor mathor

      @woooo同时感谢您指出错误@(真棒)

    4. woooo woooo

      @mathor我也是在学习,感谢博主分享学习经验@(真棒)

  16. wworz wworz

    博主你好,想请教一下:在seq2seq中,1到 trg_len 的for循环里,不需要当预测到终止符<END>时break跳出循环吗?还是说训练的时候不用管,真正预测的时候要break?

    1. mathor mathor

      @wworz其实break与否并不重要,因为假设预测出来的句子是 <s> i like apple <e> banana a b c <e>
      我们只需要截取第一次出现<e>以前的所有字符就行了
      ps:你想修改一下,设置为出现<e>就停止也可以

  17. 穹游 穹游

    博主,这个spacy的库怎么下载的,我下载中文库的时候总是报http错误

    1. mathor mathor

      @穹游应该要科技

    2. 穹游 穹游

      @mathor科技了没用

  18. 穹游 穹游

    outputs = torch.zeros(trg_len, batch_size, trg_vocab_size).to(self.device)
    这里面初始化的三个参数是可以随便给的吗,是不是只要有三个占位就行了

    1. mathor mathor

      @穹游当然不行,如果这三个参数随便给的话,那维度就不对了

    2. 穹游 穹游

      @mathor我觉得没关系,你看看Seq2Seq代码,它的output[t] = de_ouput,这个output在你的代码中只是用来存储了de_output,并没有在循环中参与计算,既然只是用来参与存储我为啥还要给他三个参数呢,不知道我的理解对不对

    3. mathor mathor

      @穹游不对,我不知道怎么说服你,要不你自己试着写一个三维的list赋值试一下就知道了

    4. 穹游 穹游

      @mathor我懂什么意思了,理解错了。博主什么时候能把Luong机制再实现一下啊,那就太好了

    5. mathor mathor

      @穹游我感觉没什么必要了,因为我在Transformer里面已经实现过了

  19. 改个什么昵称呢 改个什么昵称呢

    博主,你好~ 请问你文章里面的attention机制是不是BahdanauAttention?

    1. mathor mathor

      @改个什么昵称呢是的

    2. 改个什么昵称呢 改个什么昵称呢

      @mathor嗐,怪不得,我对着你的视频,去看Luong的代码,看了半天才发现不太对劲#(喜极而泣)

  20. kou kou

    博主,在Decoder部分的实现代码中,使用的是n_layers=1,得到的dec_hidden维度是[1, bathc_size, dec_hid_dim],那么在return的时候squeeze可以去掉这一维,如果n_layers不是 1的话,就不能直接squeeze了,是不是先取 dec_hid_dim 的最后一层结果,然后再squeeze呢?

    dec_hidden = [n_layers * num_directions, batch_size, dec_hid_dim]

    dec_output, dec_hidden = self.rnn(rnn_input, s.unsqueeze(0))
    .......
    return pred, dec_hidden.squeeze(0)

    1. mathor mathor

      @kou如果不是1的话,可以取最后一层,也可以说将每一层求一个平均torch.mean(),各种方式都可以

  21. 穹游 穹游

    博主,在train函数中下面几句话是什么意思?

    pred = model(src, trg) pred_dim = pred.shape[-1] # trg = [(trg len - 1) * batch size] # pred = [(trg len - 1) * batch size, pred_dim] trg = trg[1:].view(-1) pred = pred[1:].view(-1, pred_dim) loss = criterion(pred, trg)
  22. ever ever

    想问下楼主。模型训练好后,预测的话根本不知道trg。如何进行预测呢。在seq模型中?

    1. mathor mathor

      @ever输入开始标识即可

  23. 彼得潘 彼得潘

    感谢作者。。。救我一命,这自己看要我命啊QAQ

  24. Jones Jones

    博主,我有个问题:我发现pytorch中双向LSTM/GRU的输出结果(output而不是hidden)似乎就是已经拼接好了的,而不需要另外做torch.concat的操作。不知道是不是我搞错了。。挺急着,希望能得到解答!(希望能发到我邮箱,谢谢您~)@(太开心)

    1. mathor mathor

      @Jones随意,hidden和output都包含我们所需要的东西,所以到底用哪个都行

  25. nancy nancy

    博主,请问一下,
    现有的模型,使用了数据集的输出语句(decoder_input)进行训练。但如果要进行模型推理的话,只有输入语句(encoder_input),该如何给模型灌入输出语句呢

    1. mathor mathor

      @nancy推理的时候,decoder输入的是开始标识,让他不断地生成,最后截取到结束标识为止即可

    2. nancy nancy

      @mathor但是模型使用了teacher_force 策略,teacher_force< teacher_forcing_ratio的时候,不是用上一序列的输出作为decoder输入,这种情况怎么处理呢?

    3. mathor mathor

      @nancy推理的时候就不要用teacher forcing机制,只有训练的时候才用

    4. nancy nancy

      @mathor那这个该如何实现呢?训练的过程已经把模型结构确定了呢

    5. mathor mathor

      @nancy加一个判断就行了

    6. nancy nancy

      @mathor好的,谢谢博主,博客写的很清楚

  26. Yang Yang

    首先感谢楼主分享。您原文中“现在我们的 S0 是二维的,因此需要将 S0 的维度转为三维,并且还要调整各个维度上的值。首先我通过一个全连接神经网络,将 S0 的维度变为 [batch_size, dec_hid_dim]''。为什么要用全连接网络来实现呢?不能直接通过.view()来实现维度的转换吗?还是用全连接来实现预测效果会更好?

    1. mathor mathor

      @Yang都可以,具体效果空口无凭,还是得做实验看

    2. Yang Yang

      @mathor还有一个问题,您原文中:
      # enc_hidden = [n_layers * num_directions, batch_size, hid_dim]
      s = torch.tanh(self.fc(torch.cat((enc_hidden[-2,:,:], enc_hidden[-1,:,:]), dim = 1)))
      拼接张量时,应该是dim = 2吧,在hid_dim这个维度进行拼接,拼接后的维度才为
      [n_layers num_directions, batch_size, hid_dim 2] 才和后面的全连接层self.fc = nn.Linear(enc_hid_dim * 2, dec_hid_dim)匹配。

    3. mathor mathor

      @Yang有一点您没注意到,我举个例子
      假设a这个变量是三维的[batch, seq_len, dim]
      当我使用b = a[-2, :, :],此时b这个变量就是两维的了,同时"dim"在b中的位置就不再是2,而是1

    4. Yang Yang

      @mathor嗯嗯,我反应过来了。 感谢提醒!

  27. zyj zyj

    博主你好,代码里Decoder这个类里的dec_input的维度是[batch_size]吗?是不是错误了呀?应该是[batch_size, src_len]吧?

    1. mathor mathor

      @zyj请仔细看一下decoder的输入是什么

    2. zyj zyj

      @mathordec_input的输入应该也是一个batch的句子的序列吧,为啥只有batch_size这一个维度啊?

    3. mathor mathor

      @zyj因为输入的只有<sos>这个标识,不是一句话,而是一个单词,所以正确的维度应该是[batchsize, 1],但是这个1有没有都无所谓,所以直接就是[batchsize]

    4. zyj zyj

      @mathor我好像有点明白了,您这个地方是一个单词一个单词的放入decoder中是吧?那是不是也可以直接把整个句子放进去呢?就像您的seq2seq模型的那样的写法。

  28. deric deric

    这里的GRU在处理不等长数据编码的时候,是不是把pad也进行编码了,这样得到的句子表示会不会有问题

    1. Wonderfulrush Wonderfulrush

      @deric博主你好!我在使用这一份代码做一个生成式摘要的实验,训练阶段loss收敛到了0.5左右。而在evaluate阶段,我通过

      summary_ids=torch.max(pred,2)[1] summary_ids=summary_ids.transpose(0,1) #print(summary_ids.shape) for sen_ids in summary_ids: summary_list.append([tokenizer.decode(g, skip_special_tokens=False, clean_up_tokenization_spaces=True) for g in sen_ids])

      进行结果的解码,但解码出来的结果相当差,完全没有正常的文字,都是乱码和毫无逻辑的结果,想问下大概可能是什么原因呢?我的sos设计为0,eos设计为2,pad设计为1,输入的训练Input和target的文本都在开头和结尾含有bos与eos。
      希望能得到您的帮助

    2. Wonderfulrush Wonderfulrush

      @Wonderfulrush这部分回错人了,不好意思

  29. Wonderfulrush Wonderfulrush

    博主你好,我又发现一个问题,我把你的模型部分的代码放在谷歌云盘上跑的时候,发现loss那一部分的requieres_grad没有为true,下面loss回传的时候也报错了。是我这里哪里设置和环境有问题吗?
    我之前运行的时候loss都能正常回传和训练,但又不知道为什么突然不行了。

  30. 少壮壮 少壮壮

    博主您好,请问为什么看不到博客中的图片呢?

    1. mathor mathor

      @少壮壮刷新

  31. 111 111

    想请教一下博主decoder部分的输入x具体是指啥呢,博主在博客里提到是emb(yt)不知道是否可以解释一下是什么,还有decoder的输出为啥是(dec_output, c, embedded)三个拼接在一起而不是单独的dec_output呢

  32. 超哥 超哥

    attention中的v这一层是不是可以取消呢?

  33. 超哥 超哥

    attention中,为什么要拼接呢,数学原理是什么?

  34. 青芒 青芒

    公式怎么都是[Math Processing Error]了

    1. mathor mathor

      @青芒刷新

    2. 青芒 青芒

      @mathor好的可以了,谢谢博主

  35. 周

    楼主大大,请问如何做时间序列的呢,就比如预测有明天有多少人去电影院给了过去多少天的人数,感觉和NLP这种不太一样

    1. mathor mathor

      @周论文informer是研究时间序列问题的

  36. 王

    最后一个公式,需要将三个变量全部拼接在一起,然后通过一个全连接神经网络,得到最终的预测。我们先分析下这个三个变量的维度, emb(yt)的维度是 [batch_size, 1, emb_dim], c的维度是 [batch_size, 1, enc_hid_dim],st 的维度是 [1, batch_size, dec_hid_dim],......
    原文写的‘ c的维度是 [batch_size, 1, enc_hid_dim]’,是不是笔误写错了,应该是c的维度是 [batch_size, 1, enc_hid_dim*2]’吧

    1. mathor mathor

      @王是笔误,已修改,感谢提醒