使用Encoder-Decoder框架实现Seq2Seq模型

释放双眼,带上耳机,听听看~!
本文介绍了如何使用Encoder-Decoder框架和pytorch内置的GRU实现Seq2Seq模型,详细解释了代码思路和实现过程。

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情

前边刚说了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[0]的形状: (num_layers, batch_size, num_hiddens)

      • 这里解释一下为什么output叫最后的隐状态,因为可能涉及多层啊之类的,结果取得都是最后一层的,比如深度RNN,都是浅层向深层接续计算的。那为什么state是最后时间步的隐状态,因为state是存储最后一个时间步所有的隐状态,有几层存几层。
        使用Encoder-Decoder框架实现Seq2Seq模型

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)
        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,一共有两个输出,所有时间步最后一层的隐状态和最后一个时间步所有的隐状态。这里是取最后一个时间步的隐状态来进行初始化解码器的隐状态。

使用Encoder-Decoder框架实现Seq2Seq模型

  • 然后是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)

    • 最后将结果返回。

    使用Encoder-Decoder框架实现Seq2Seq模型

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, :] < 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])
        

        输出为:

        使用Encoder-Decoder框架实现Seq2Seq模型

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

        使用Encoder-Decoder框架实现Seq2Seq模型

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

  • 测试一下嗷

    对于二维的:

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

    使用Encoder-Decoder框架实现Seq2Seq模型

    对于三维的:

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

    使用Encoder-Decoder框架实现Seq2Seq模型

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,)
本网站的内容主要来自互联网上的各种资源,仅供参考和信息分享之用,不代表本网站拥有相关版权或知识产权。如您认为内容侵犯您的权益,请联系我们,我们将尽快采取行动,包括删除或更正。
AI教程

NVIDIA-SMI 时钟频率(CLOCK)详解

2023-12-18 19:38:14

AI教程

Google大语言对话模型Bard试用申请及体验

2023-12-18 19:53:14

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索