(7-3 )PyTorch机器翻译系统(2)

7.3.3  实现Seq2Seq 模型

循环神经网络(RNN)是在序列上运行并将其自身的输出用作后续步骤的输入的网络。序列到序列网络或 seq2seq 网络或编码器解码器网络是由两个称为编码器和解码器的 RNN 组成的模型。编码器读取输入序列并输出单个向量,而解码器读取该向量以产生输出序列。如图7-5所示。

7-5  Seq2Seq结构

与使用单个 RNN 进行序列预测(每个输入对应一个输出)不同,seq2seq 模型使我们摆脱了序列长度和顺序的限制,这使其非常适合两种语言之间的翻译。考虑一下下面句子的翻译过程:

Je ne suis pas le chat noir -> I am not the black cat

在输入句子中的大多数单词在输出句子中具有直接翻译,但是顺序略有不同,例如chat noir和black cat。由于采用ne/pas结构,因此在输入句子中还有一个单词。直接从输入单词的序列中产生正确的翻译将是困难的。通过使用 seq2seq 模型,在编码器中创建单个向量,在理想情况下,该向量将输入序列的“含义”编码为单个向量—在句子的 N 维空间中的单个点。

1. 编码器

seq2seq 网络的编码器是 RNN,它为输入句子中的每个单词输出一些值。 对于每个输入字,编码器输出一个向量和一个隐藏状态,并将隐藏状态用于下一个输入字。编码过程如图7-6所示。

7-6  编码过程

编写类EncoderRNN ,它是一个循环神经网络(Recurrent Neural Network,RNN)的编码器。这个类定义了编码器的结构和前向传播逻辑。编码器使用嵌入层将输入序列中的单词索引映射为密集向量表示,并将其作为 GRU 层的输入。GRU 层负责对输入序列进行编码,生成输出序列和隐藏状态。编码器的输出可以用作解码器的输入,用于进行序列到序列的任务,例如机器翻译。函数initHidden()用于初始化隐藏状态张量,作为编码器的初始隐藏状态。对应的实现代码如下所示:

class EncoderRNN(nn.Module):
    def __init__(self, input_size, hidden_size):
        super(EncoderRNN, self).__init__()
        self.hidden_size = hidden_size

        self.embedding = nn.Embedding(input_size, hidden_size)
        self.gru = nn.GRU(hidden_size, hidden_size)

    def forward(self, input, hidden):
        embedded = self.embedding(input).view(1, 1, -1)
        output = embedded
        output, hidden = self.gru(output, hidden)
        return output, hidden

    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

2. 解码器

解码器是另一个 RNN,它采用编码器输出向量并输出单词序列来创建翻译。

1简单解码器

在最简单的 seq2seq 解码器中,仅使用编码器的最后一个输出。最后的输出有时称为上下文向量,因为它从整个序列中编码上下文。 该上下文向量用作解码器的初始隐藏状态。在解码的每个步骤中,为解码器提供输入标记和隐藏状态。 初始输入标记是字符串开始<SOS>标记,第一个隐藏状态是上下文向量(编码器的最后一个隐藏状态)。如图7-7所示。

7-7  简单解码器

定义类 DecoderRNN,这是一个循环神经网络(Recurrent Neural Network,RNN)的解码器。这个类定义了解码器的结构和前向传播逻辑。解码器使用嵌入层将输出序列中的单词索引映射为密集向量表示,并将其作为 GRU 层的输入。GRU 层负责对输入序列进行解码,生成输出序列和隐藏状态。解码器的输出通过线性层进行映射,然后经过 softmax 层进行概率归一化,得到最终的输出概率分布。initHidden 方法用于初始化隐藏状态张量,作为解码器的初始隐藏状态。对应的实现代码如下所示:

class DecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size):
        super(DecoderRNN, self).__init__()
        self.hidden_size = hidden_size

        self.embedding = nn.Embedding(output_size, hidden_size)
        self.gru = nn.GRU(hidden_size, hidden_size)
        self.out = nn.Linear(hidden_size, output_size)
        self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, input, hidden):
        output = self.embedding(input).view(1, 1, -1)
        output = F.relu(output)
        output, hidden = self.gru(output, hidden)
        output = self.softmax(self.out(output[0]))
        return output, hidden

    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

2注意力解码器

如果仅上下文向量在编码器和解码器之间传递,则该单个向量承担对整个句子进行编码的负担。使用注意使解码器网络可以针对解码器自身输出的每一步,“专注”于编码器输出的不同部分。 首先,计算一组注意力权重,将这些与编码器输出向量相乘以创建加权组合。 结果(在代码中称为attn_applied)应包含有关输入序列特定部分的信息,从而帮助解码器选择正确的输出字。如图7-8所示。

7-8  注意力解码器

另一个前馈层attn使用解码器的输入和隐藏状态作为输入来计算注意力权重。 由于训练数据中包含各种大小的句子,因此要实际创建和训练该层,我们必须选择可以应用的最大句子长度(输入长度​​,用于编码器输出)。最大长度的句子将使用所有注意权重,而较短的句子将仅使用前几个。如图7-9所示。

7-9  前馈层attn

编写类AttnDecoderRNN实现具有注意力机制的解码器,对应的实现代码如下所示:

class AttnDecoderRNN(nn.Module):
    def __init__(self, hidden_size, output_size, dropout_p=0.1, max_length=MAX_LENGTH):
        super(AttnDecoderRNN, self).__init__()
        self.hidden_size = hidden_size
        self.output_size = output_size
        self.dropout_p = dropout_p
        self.max_length = max_length

        self.embedding = nn.Embedding(self.output_size, self.hidden_size)
        self.attn = nn.Linear(self.hidden_size * 2, self.max_length)
        self.attn_combine = nn.Linear(self.hidden_size * 2, self.hidden_size)
        self.dropout = nn.Dropout(self.dropout_p)
        self.gru = nn.GRU(self.hidden_size, self.hidden_size)
        self.out = nn.Linear(self.hidden_size, self.output_size)

    def forward(self, input, hidden, encoder_outputs):
        embedded = self.embedding(input).view(1, 1, -1)
        embedded = self.dropout(embedded)

        attn_weights = F.softmax(
            self.attn(torch.cat((embedded[0], hidden[0]), 1)), dim=1)
        attn_applied = torch.bmm(attn_weights.unsqueeze(0),
                                 encoder_outputs.unsqueeze(0))

        output = torch.cat((embedded[0], attn_applied[0]), 1)
        output = self.attn_combine(output).unsqueeze(0)

        output = F.relu(output)
        output, hidden = self.gru(output, hidden)

        output = F.log_softmax(self.out(output[0]), dim=1)
        return output, hidden, attn_weights

    def initHidden(self):
        return torch.zeros(1, 1, self.hidden_size, device=device)

对类参数的具体说明如下:

  1. hidden_size:隐藏状态的维度大小。
  2. output_size:输出的词汇表大小(即词汇表中的单词数量)。
  3. dropout_p:dropout 概率,用于控制在训练过程中的随机失活。
  4. max_length:输入序列的最大长度。

对__init__()方法的具体说明如下:

  1. 初始化函数,用于创建并初始化 AttnDecoderRNN 类的实例。
  2. 调用父类的初始化方法 super(AttnDecoderRNN, self).__init__()。
  3. 将 hidden_size、output_size、dropout_p 和 max_length 存储为实例属性。
  4. 创建一个嵌入层(Embedding layer),用于将输出的单词索引映射为密集向量表示。该嵌入层的输入大小为 output_size,输出大小为 hidden_size。
  5. 创建一个线性层 attn,用于计算注意力权重。该线性层将输入的两个向量拼接起来,然后通过一个线性变换得到注意力权重的分布。
  6. 创建一个线性层 attn_combine,用于将嵌入的输入和注意力应用的上下文向量进行结合,以生成解码器的输入。
  7. 创建一个 dropout 层,用于在训练过程中进行随机失活。
  8. 创建一个 GRU 层,用于处理输入序列。该 GRU 层的输入和隐藏状态的大小都为 hidden_size。
  9. 创建一个全连接线性层,用于将 GRU 层的输出映射到输出大小 output_size。

forward ()方法的具体说明如下:

  1. 前向传播函数,用于对输入进行解码并生成输出、隐藏状态和注意力权重。
  2. 接受输入张量 input、隐藏状态张量 hidden 和编码器的输出张量 encoder_outputs 作为输入。
  3. 将输入张量通过嵌入层进行词嵌入,然后进行随机失活处理。
  4. 将嵌入后的张量与隐藏状态张量拼接起来,并通过线性层 attn 计算注意力权重的分布。
  5. 使用注意力权重将编码器的输出进行加权求和,得到注意力应用的上下文向量。
  6. 将嵌入的输入和注意力应用的上下文向量拼接起来,并通过线性层 attn_combine 进行结合,得到解码器的输入。
  7. 将解码器的输入通过激活函数 ReLU 进行非线性变换。
  8. 将变换后的张量作为输入传递给 GRU 层,得到输出和更新后的隐藏状态。
  9. 将 GRU 的输出通过线性层 out 进行映射,并通过 LogSoftmax 函数计算输出的概率分布。
  10. 返回输出、隐藏状态和注意力权重。

对initHidden()方法的具体说明如下:

  1. 用于初始化隐藏状态张量,作为解码器的初始隐藏状态。
  2. 返回一个大小为 (1, 1, hidden_size) 的全零张量,其中 hidden_size 是隐藏状态的维度大小。

未完待续

  • 23
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码农三叔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值