Seq2Seq

Seq2Seq的pytorch实现

Seq2Seq基本结构:Encoder -> Decoder

来看torch官方给出的模型图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PI1qci3g-1653649091522)(https://z3.ax1x.com/2021/04/29/gF2xtP.png#shadow)]

首先来看Encoder:

模型上: Encoder是一个循环神经网络,具体可以是LSTM,GRU

Encoder的输入:[batch_size,seq_len],不妨叫它batch_x那么batch_x[0]指的就是第一个句子中单词索引构成的一个向量,batch_x[0][0]的值就是第一个句子的第一个单词在词典中对应的索引

Encoder的构成: Embedding层 ,GRU层.

Embedding层用于将句子中的单词转换为一个向量(Word2Vec词向量也可以)

nn.Embedding(vocab_size,emdedding_dim)构造时需要两个参数,一个是单词的数目,一个是向量大小

GRU层就是编码器核心部分,用于将序列的信息集中到最后的隐藏层状态里

nn.GRU(emdedding_dim,hidden_size,num_layer,batch_first)构造时需要的参数,一个是输入的大小,也就是一个单词映射到向量的大小,然后是隐藏层大小,最后是隐藏层层数

下面来看Encoder的代码部分

class EncoderRNN(nn.Module):
    """不含注意力的RNN编码器"""

    def __init__(self, vocab_size, hidden_size):
        super(EncoderRNN, self).__init__()
        self.hidden_size = hidden_size
        # 词嵌入 :vocab_size个单词 嵌入到hidden_size维中  而后可以根据 index找到对应词的词嵌入(也可以换成词向量)
        self.embedding = nn.Embedding(vocab_size, embedding_dim=hidden_size)
        # 单向GRU作编码器: 默认1层
        self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)

    def forward(self, batch_seq, hidden):
        """
        :param batch_seq    :[batch_size,seq_len]  x[0][0] 指第一句话 第一个词的索引
        :param hidden       :[?,?]上一次的隐藏层状态
        """
        # 词嵌入 [batch_size,seq_len,embedding_dim] x[0][0]是这个词的向量表示
        embedded = self.embedding(batch_seq)
        # GRU编码
        # out的形状与embedding一致
        # hidden: [num_layer,batch_size,n_hidden]
        out, hidden = self.gru(embedded, hidden)
        return out, hidden

    def init_hidden(self, num_layer, batch_size):
        return torch.zeros(num_layer, batch_size, self.hidden_size)

Encoder的前向传播过程

首先是两个输入的形状和含义

batch_seq指输入的句子,形状为[batch_size,seq_len]

hidden指初始隐藏层状态h0,形状为[num_layer,batch_size,n_hidden],batch_size在第二维的原因是torch的RNN输出的hidden的batch都在第二维,比如hidden[0,0]就是第一层隐藏层的第一个batch对应隐藏层内容

然后是词嵌入

将batch_seq传入Embedding后,输出的形状变为[batch_size,seq_len,embedding_dim],根据词的索引将词变成了一个embedding_dim维的向量

最后是GRU

需要注意的是,nn.GRU一次就会处理完一个序列,不会返回中间状态.

GRU的输入形状是[batch_size,seq_len,embedding_dim]

GRU的输出有两个,一个是对输入的预测,一个是最后时刻的隐层状态

output的形状:[batch_size,seq_len,embedding_dim] 与 输入形状一模一样,output[0,0]表示的含义是第一个句子中第一个单词的预测输出(是一个embedding_dim大小的向量 可以对应到单词索引)

hidden的形状:[num_layer,batch_size,n_hidden]是最后时刻的隐藏层状态,它汇集了batch_size个句子的特征信息,而后会作为解码器的初始隐层输入

下面来看Decoder

模型上:Decoder更像一个全连接网络

Decoder的输入:[batch_size,seq_len]与encoder相同

Decoder的组成:Embedding,GRU,classification,softmax

Embedding与Encoder相同,跳过

GRU与Encoder相同,跳过

classification+softmax:一个全连接层,用于将GRU的ouput变为一个概率分布,用于输出预测的单词

Decoder代码

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)
        # 单向GRU
        self.gru = nn.GRU(hidden_size, hidden_size, batch_first=True)
        self.classification = nn.Linear(hidden_size, output_size)
        self.softmax = nn.LogSoftmax(dim=1)

    def forward(self, x, hidden):
        """
        :param x: []
        :param hidden:
        :return:
        """
        # 输出 [batch_size,seq_len,embedding_dim]
        embedded = self.embedding(x)
        embedded = F.relu(embedded)
        # output:[batch_size,seq_len,embedding_dim]
        output, hidden = self.gru(embedded, hidden)
        # [batch,seq_len,vocab_size] [0][0] 表示第一个句子 第一个词 预测的结果
        output = self.classification(output)
        # 对预测的结果做一个LogSoftmax概率统计
        output = self.softmax(output.squeeze().view(1, -1))
        output = output.unsqueeze(0)
        return output, hidden

    def init_hidden(self, num_layer, batch_size):
        return torch.zeros(num_layer, batch_size, self.hidden_size)

Decoder的前向传播过程:

!!!需要注意的是,与Encoder不同,虽然输入形状为[batch_size,seq_len],但是seq_len必须为1,因为我们需要一步一步的获取Decoder的输出作为下一步的输入

!!!在Encoder中我们是将所有时刻的输入一次性给了GRU,而Decoder不能这样,因为Decoder并没有外部输入,我们只构造一个开始时刻的SOS标志给GRU,而后的输入都是上一时刻的输出.

好,我们继续看前向传播,首先是对输入进行编码,而后是ReLU激活

使用当前时间步的输入,和隐藏层状态作为输入传入GRU,GRU实际只做了一次预测,所以输出为

output:[batch_size,1,embedding_dim]

而后使用全连接对output进行降维,将output置为(1,vocab_size)的形状传入LogSoftmax,计算当前时间步预测的结果概率分布

Decoder的前向传播实际只对一个输入进行了预测,在训练时我们应如此做:

decoder_input = torch.tensor([[SOS_TOKEN]] * 1, dtype=torch.long).view(1, 1)
decoder_hidden = ht
for index in range(target_len):
    decoder_output, decoder_hidden = decoder(decoder_input, decoder_hidden)
    _, top_index = decoder_output.topk(1)
    # 取出当前预测概率最大值的索引 作为下一个输入 [batch:1,seq_len:1]
    decoder_input = top_index.squeeze().detach().view(1, 1)
    # 这里很失败啊 解码器不该处理seq_len的
    # decoder_output[0, 0] 取第一个batch的第一个词的预测概率(实际也就一个词)
    # target[0,index] 取第一个batch 对应位置的真实输出做对比
    loss += criterion(decoder_output[0, 0], target[0, index])
    if decoder_input.item() == EOS_TOKEN:
        break

初始时间步,我们人为构造一个起始状态,使用SOS_TOKEN作为起始字符索引构造Tensor

而后使用编码器最后时间步隐层状态ht作为初始输入

之后我们让解码器循环训练,根据其返回的概率分布,我们计算出其预测的单词索引,再将改单词索引变为Tensor作为下一时间步的输入,直到达到真实句子长度或者其预测出EOS_TOKEN结束符为止

总结

一个起始状态,使用SOS_TOKEN作为起始字符索引构造Tensor

而后使用编码器最后时间步隐层状态ht作为初始输入

之后我们让解码器循环训练,根据其返回的概率分布,我们计算出其预测的单词索引,再将改单词索引变为Tensor作为下一时间步的输入,直到达到真实句子长度或者其预测出EOS_TOKEN结束符为止

总结

Seq2Seq2的核心在于解码器,解码器要使用自己上一步的输出作为下一步的输入,这是关键点
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值