MENU

Seq2Seq(Attention)的PyTorch实现

July 2, 2020 • Read: 107207 • 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

146 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哦哦

    8. ggapplyer ggapplyer

      @mathor那请问时间序列预测的话还需要设置<sos><eos>吗?

    9. mathor mathor

      @ggapplyer不需要

    10. ggapplyer ggapplyer

      @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好的,谢谢博主,博客写的很清楚

    7. 谷雨 谷雨

      @nancy大佬 能请教一些问题吗?

  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

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

  37. 谷雨 谷雨

    请问githup中 transformer_torch.py就是全部程序吗?

    1. mathor mathor

      @谷雨不是,是seq2seq(attention).py

    2. 谷雨 谷雨

      @mathor博主为什么h0的维度的第一个维度是src_len啊

  38. 谷雨 谷雨

    楼主 视频中hidden的维度是[2,batch_size, en_hid_dim2], hidden[-1,:,:]不是的取了2层中最后一层嘛 为什么是你讲的是取了h12和ht2啊,维度就变成了[batch_size, en_hid_dim],我觉得应该是[batch_size, en_hid_dim2] ,我想的是 就想一个三维矩阵 你取了一层变成了二维 不应该二维中的列向量也减半了啊

  39. 谷雨 谷雨

    楼主 有可视化代码吗 就是输入德文 翻译英文的代码

    1. mathor mathor

      @谷雨我的建议是,有问题请先自己解决

  40. Yiming Zhang Yiming Zhang

    谢谢您的分享!想请问如果是时间序列预测,那这个dec_output, s = self.decoder(dec_input, s, enc_output)里面的dec_input应该是什么呀?以及维度应该是怎样的? 多谢指教!

  41. Rachel Duan Rachel Duan

    请问下,Encoder中最后的s为什么要torch.tanh啊?我看公式里没有这一步,是在和H拼接,通过Linear后才torch.tanh的呀?

  42. Astronaut Astronaut

    您好,我在试着跟着您学习这份代码的时候,在dec_output, s = self.decoder(dec_input, s, enc_output)这个地方,发现每一次for里面的s都会在attention模块扩充一维。请问这应该怎么解决呢?我是在自学这个东西,麻烦您

  43. 123456 123456

    您好 在真正翻译的时候没有目标值 Seq2seq 的trg该怎么输入?

    1. mathor mathor

      @123456仅输入<s>开始标志,采用自回归的方式,上一个时刻的输出作为下一个时刻的输入,用while True实现,直到出现</s>结束标志break

    2. 123456 123456

      @mathor非常感谢 但是我用自己的数据集训练出来的效果很差 根本组不成句子 是训练不充分吗

  44. 吴小东 吴小东

    想问一下Encoder部分最后输出output和s,s为什么要加一个tanh的激活函数那?我想不明白,求解答

  45. 吴小东 吴小东

    想问下,你在decoder中开始说,把emby和c和s进行拼接输入Linear进行预测伪代码中是这样的,但是你在真正代码是把经过gru的output和c和s进行拼接然后Linear进行预测的。我想知道到底是哪三个值进行拼接呀?

    1. 吴小东 吴小东

      @吴小东是不是那个s就是output

  46. night_walk night_walk

    希望能问一下,在您的全部代码中,其中在计算loss的时候,最后return的时候 loss/数据数目,是什么目的,能够告知一下,万分感谢

    1. mathor mathor

      @night_walk计算平均loss

    2. night_walk night_walk

      @night_walk已经知道了,感觉自己问的好傻...

  47. night_walk night_walk

    想问一下博主,在decoder的时候,一般第一个输入是<sos>,然后去计算,但是我现在的数据不是embedded过后的数据,比如说是股票数据,那我应该如何设置这里decoder的第一个数据呢,请问博主有什么见解,万分感谢

    1. mathor mathor

      @night_walk没有见解

  48. yew yew

    你好,我运行了你的seq2seq(attention).py代码,但是加载完数据后一直没有结果,不知道为什么,应该出现怎样的运行结果啊?

    1. zzz zzz

      @yew您好, seq2seq (attention).py 代码可以发我一份吗,谢谢

    2. xiaoshu xiaoshu

      @yew我也想知道!请问现在您知道了吗?到底是什么输出呢?我输入一个德语句子,会显示在文件中哪一行,但同时 SyntaxError: invalid syntax。

  49. g. g.

    细节纠错:
    Decoder里代码的实现:pred = self.fc_out(torch.cat((dec_output, c, embedded), dim = 1))
    应该去掉embedded,正确的是pred = self.fc_out(torch.cat((dec_output, c), dim = 1))

  50. xiaoshu xiaoshu

    想问下博主,torchtext版本是?运行结果显示SyntaxError: invalid syntax

  51. xiaobai xiaobai

    博主,您好,我是刚开始研究文本摘要泽哥方向,想问您几个问题,

    就是数据预处理阶段,对于这个数据进行分词和分字 在您这个本次讲解中是按照字的划分,src_len=字的个数,那么文本摘要中的分词操作它的意义是什么,就直接取掉特殊符号和停用词按字分不就可以了吗?这块不太懂就是在seq2seq+attention中 讲述了单个编码器-解码器,在我目前已知的论文中有些是用了双编码器从不同层次来分析文本信息,那当两个编码器处理之后,是如何来解决这个[C,S,X]也就是这个隐藏状态,输出,和decoder的输入,这个不知道您能否讲一下。
  52. xiaobai xiaobai

    你好 大佬 还有一个问题 如果采用双编码器构建seq2seq 它的最后隐藏状态该怎么算

  53. xiaobai xiaobai

    trg_vocab_size = self.decoder.output_dim 这句话是什么意思?

  54. Pytorch实战_Seq2seq模型_Johngo学长

    [...]李宏毅老师的课程在此处并没有给出具体的代码,需要大家自己补充。大家可以参考这篇文章 Seq2Seq (Attention) 的 PyTorch 实现 或者B站的视频 PyTorch35——基于注意力机制的Seq2Seq的PyTorch实现示例。[...]

  55. Pytorch实战_Seq2seq模型 R11; 源码巴士

    [...]李宏毅老师的课程在此处并没有给出具体的代码,需要大家自己补充。大家可以参考这篇文章 Seq2Seq (Attention) 的 PyTorch 实现 或者B站的视频 PyTorch35——基于注意力机制的Seq2Seq的PyTorch实现示例。[...]

  56. 文本摘要小白 文本摘要小白

    如果在seq2seq+attention前面加入了BERT预训练模型 那么在代码中,decoder的词的输入应该是普通的word_embedding 还是使用bert的词汇表 token_to_ids 的word_embedding

  57. ggapplyer ggapplyer

    你好,请看一下Encoder类的第29行,把dim=1进行torch.cat()那么维度不是变成了[2*batch_size, hid_dim]吗?这输入到fc层中是不是错了呀?我现在觉得cat()的是dim=2,不知道我的说法是否正确,请多指教!

    1. ggapplyer ggapplyer

      @ggapplyer我发现自己的错误啦,打扰了!

    2. mathor mathor

      @ggapplyer没事儿