0
点赞
收藏
分享

微信扫一扫

代码实现seq2seq 序列到序列 | #51CTO博主之星评选#

前边刚说了encoder-decoder框架,也写了大体的代码思路。但是有思路不代表能写出东西来,今天就使用encoder-decoder来实现一个seq2seq。

import collections
import math
import torch
from torch import nn
from d2l import torch as d2l

常规操作导个包。

编码器

class Seq2SeqEncoder(d2l.Encoder):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
                 dropout=0, **kwargs):
        super(Seq2SeqEncoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,dropout=dropout)

    def forward(self, X, *args):
        X = self.embedding(X)
        X = X.permute(1, 0, 2)
        output, state = self.rnn(X)
        return output, state

定义一下seq2seq的encoder,在这里是继承了写文章 - Encoder-Decoder框架 - 掘金 (juejin.cn)的encoder。

  • __init__中先进行初始化,作为子类新增加了自己的embedding层,这里RNN层使用的是pytorch自带的GRU(门控循环单元)。
  • forward定义前向传播:
    • 输出'X'的形状:(batch_size, num_steps, embed_size) 即第一维度是batch的大小,第二维度是时间步的长度,第三维度是embedding的大小。
    • 第二句中,我们使用permuteX的维度进行修改,即将其原来的前两个维度进行调换,改成时间步长度、批量大小、embedding大小。因为在循环神经网络模型中,第一个轴对应于时间步(pytorch内置的GRU的要求。当然可以修改的,但是那样写更麻烦,具体可以看文档GRU — PyTorch 1.11.0 documentation)。
    • 第三步第四步是计算并返回encoder的隐状态。在这里需要注意一下,encoder是不需要输出的,他只需要将输入变为一个对应的矩阵即可。所以在这里所谓的output是所有时间步最后的隐状态,而state是最后一个时间步最后的隐状态。
      • output的形状: (num_steps, batch_size, num_hiddens)
      • state的形状: (num_layers, batch_size, num_hiddens)
      • 这里解释一下为什么output叫最后的隐状态,因为可能涉及多层啊之类的,结果取得都是最后一层的,比如深度RNN,都是浅层向深层接续计算的。那为什么state是最后时间步的隐状态,因为state是存储最后一个时间步所有的隐状态,有几层存几层。
        image.png
encoder = Seq2SeqEncoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
encoder.eval()
X = torch.zeros((4, 7), dtype=torch.long)
output, state = encoder(X)
output.shape,state.shape

现在我们试一下子我们的encoder。

  • 第一句设定默认的参数,假装我们预处理之后的字典长度为10,word embedding的大小为8,隐藏层大小为16,有俩层。
  • 第二句是使用eval()将其转模式。这句不写也行的其实。
  • 现在我们随便搞个X,假设大小为4*7的矩阵,用torch.zeros将其初始化为0。
  • 然后我们取出来outputstate看一下他们俩的形状是啥。因为我是在jupyter notebook里写的,所以最后一句是个输出格式,不用print,如果你们用别的IDE写的记得Print。

输出是:

(torch.Size([7, 4, 16]), torch.Size([2, 4, 16]))

自己对一下维度,是可以对上的。

解码器

class Seq2SeqDecoder(d2l.Decoder):
    def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,dropout=0, **kwargs):
        super(Seq2SeqDecoder, self).__init__(**kwargs)
        self.embedding = nn.Embedding(vocab_size, embed_size)
        self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers, dropout=dropout)
        self.dense = nn.Linear(num_hiddens, vocab_size)

    def init_state(self, enc_outputs, *args):
        return enc_outputs[1]

    def forward(self, X, state):
        X = self.embedding(X).permute(1, 0, 2)
        context = state[-1].repeat(X.shape[0], 1, 1)
        X_and_context = torch.cat((X, context), 2)
        output, state = self.rnn(X_and_context, state)
        output = self.dense(output).permute(1, 0, 2)
        # `output`的形状: (`batch_size`, `num_steps`, `vocab_size`)
        # `state[0]`的形状: (`num_layers`, `batch_size`, `num_hiddens`)
        return output, state

来看一下decoder部分。这个也是继承了上一篇文章里写的decoder。作为子类看看他都增加了什么新功能。

  • 依旧从__init__开始说:

    • 这里也有个embedding,注意这里的embedding是decoder自己的,编码器解码器不共享词嵌入哦!
    • rnn这里依旧是使用GRU,但是注意这里第一项参数和编码器不一样了,这里是变成了embed_size + num_hiddens,这里待会看下边forward函数时候就懂了。
    • 注意这里多了个self.dense,这个是decoder的输出。是真的输出!decoder是要做输出的!!!不要迷糊!
  • 看一下init_state函数:

    注意这里参数列表写的是enc_outputs,应该能看出来是把encoder的outputs拿来做隐状态初始化的吧。
    然后函数里是取enc_outputs[1],对照一下encoder,一共有两个输出,所有时间步最后一层的隐状态和最后一个时间步所有的隐状态。这里是取最后一个时间步的隐状态来进行初始化解码器的隐状态。image.png

  • 然后是forward函数。

    • 输出'X'的形状:(batch_size, num_steps, embed_size),再对其进行维度的交换,将batch_size批量大小和num_steps时间步大小进行交换,使其符合pytorch内置GRU的参数要求。
    • 第二步是取state的最后一项,对其广播生成context,使其具有与X相同的num_steps
      知不知道为什么取state的最后一项要用state[-1],因为我们有多少层是自己决定的,这里我们暂时是设置的2,所以虽然在这里例子里可以用state[1]取得最后一项,但是如果换个层数就不成立了。
    • 第三步X_and_context将解码器的输入Xstate最后一个进行拼接。这里不是单纯的对其输入进行直接计算,而是结合编码器的结果进行计算。所以上边的GRU第一个参数变为了embed_size + num_hiddens
    • 进行计算获得decoder的最后一层的所有隐状态和最后一个时间步的隐状态。
    • 然后使用所有时间步最后的隐状态获得输出dense,并对其维度进行修改。

      • output的形状: (batch_size, num_steps, vocab_size)

      • state的形状: (num_layers, batch_size, num_hiddens)
    • 最后将结果返回。
      image.png
decoder = Seq2SeqDecoder(vocab_size=10, embed_size=8, num_hiddens=16,num_layers=2)
decoder.eval()
state = decoder.init_state(encoder(X))
output, state = decoder(X, state)
output.shape, state.shape

搞一下我们的decoder。

  • 第一步传入参数。

  • 第二步使用.eval()更改其模式。
  • 第三步生成自己的初始隐状态。
  • 第四步获取输出和最后一个时间步的所有的隐状态。

    注意这里的output是真的输出,和encoder的性质不一样。

  • 最后我们输出一下二者的维度,结果是:

(torch.Size([4, 7, 10]), torch.Size([2, 4, 16]))

mask

def sequence_mask(X, valid_len, value=0):  
    maxlen = X.size(1)
    mask = torch.arange((maxlen), dtype=torch.float32,device=X.device)[None, :] < valid_len[:, None]
    X[~mask] = value
    return X

看一下这个mask函数,之前看过我其他文章的应该熟悉这个mask操作的。

  • 先看传入的参数,X待mask的矩阵,valid_len有效长度,需要传入一个向量,这个向量的长度要和X的长度一样(不是样本长度,是几个样本)。value默认是0,就是使用0进行mask,也可以更换为其他值。
  • maxlenX的第一个维度,也就是有多少列。
  • 初始化mask

    这里对其进行了几步操作我们来分析一下。将长代码拆分开相当于两句。

    • mask = torch.arange((maxlen), dtype=torch.float32,device=X.device)这里是先arange初始化mask向量。

    • mask = mask[None, :] &lt; valid_len[:, None]

      这个[None,:]有点类似于增加一个维度的torch.squeeze那种操作。

      回忆一下python的基础知识 :

      • a = b[:]就是把b的所有内容复制给a,

      • a = b[:,None]就是给b增加一个维度,b原来的内容作为a的每行第一个元素。
      • a = b[None,:]就是给b在前边增加一个维度,b原来的内容作为a的第一行。
      • 在哪里增加None就是给哪里添加一个维度。

        d = torch.arange(3)
        print(d)
        print(d[:,None])
        print(d[None,:])
        print(d[None,:,None])

        输出为:
        image.png

      使用两个增加维度之后的矩阵作比较,得到mask,值的类型为布尔值。
      image.png

  • 之后对mask取反,之后使其等于value,也就是将不需要的值设置为0。
  • 测试一下嗷

    对于二维的:

    X = torch.tensor([[1, 2, 3], [4, 5, 6]])
    sequence_mask(X, torch.tensor([1, 2]))

    image.png
    对于三维的:

    X = torch.ones(2, 3, 4)
    sequence_mask(X, torch.tensor([1, 2]), value=-1)

    image.png

class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
    def forward(self, pred, label, valid_len):
        weights = torch.ones_like(label)
        weights = sequence_mask(weights, valid_len)
        self.reduction='none'
        unweighted_loss = super().forward(pred.permute(0, 2, 1), label)
        weighted_loss = (unweighted_loss * weights).mean(dim=1)
        return weighted_loss

loss = MaskedSoftmaxCELoss()

好了上边那个mask函数其实就是为了搞一个带遮蔽效果的softmax。一些填充长度的值存在对softmax也没用,预测对了也没用,所以将其遮盖了。做法是搞一个参数weights,保留的部分设置为1,其余部分设置为0。

  • pred 的形状:(batch_size, num_steps, vocab_size)
  • label 的形状:(batch_size, num_steps)
  • valid_len 的形状:(batch_size,)
举报

相关推荐

0 条评论