多模态大模型:算法、应用与微调
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.1.2 编码器-解码器结构

我们继续深入探索Seq2Seq结构,如图1-3所示,模型的最外层一般是由一个编码器和一个解码器组成的。编码器依次对输入序列中的每个元素进行处理,然后将获取的信息压缩成一个上下文向量,之后将上下文向量发送给解码器,解码器根据输入序列和上下文向量依次生成输出序列。编码器和解码器在早期一般采用RNN模块,后来变成了LSTM(长短期记忆网络)或GRU(门控循环单元)模块,不过不管内部模块是什么,其主要特点就是可以提取时序特征信息。

图1-3 Seq2Seq结构的模型一般由一个编码器和一个解码器组成

接下来我们用PyTorch实现一个简单的Seq2Seq结构模型,并用于1.1.4节的实战案例。本次实现主要分为以下几步:首先实现Seq2Seq结构模型的编码器和解码器部分;然后在解码器中加入注意力机制;之后定义该任务的损失函数;最后进行训练和评估。

1.编码器

在编码器中,一般会先使用嵌入层(Embedding Layer)将输入序列中的每一个词元转换为特征向量,嵌入层的参数是一个vocab_size×embed_size的矩阵,其中vocab_size表示词表的大小,embed_size表示特征向量的维度。如果有多个编码器,则词向量的转换仅发生在第一个编码器的处理过程中。在底层的编码器中输入的是词向量列表,而在其他编码器中,输入的是上一层编码器的输出。所有编码器都有相同的输入来源,这些输入通常是一个大小为512的向量列表。列表的大小是可以通过设置超参数来调整的,通常设置为训练数据集中最长句子的长度。

python

class EncoderRNN(nn.Module):

  def __init__(self, input_size, hidden_size, dropout_p=0.1):

    super(EncoderRNN, self).__init__()

    self.hidden_size=hidden_size

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

    self.rnn=nn.RNN(hidden_size, hidden_size, batch_first=True)

    self.dropout=nn.Dropout(dropout_p)


  def forward(self, x):

    x=self.embedding(x)

    x=self.dropout(x)

    output, hidden=self.rnn(x)

    return output, hidden

这段代码定义了一个名为EncoderRNN的类,用于将输入的序列数据编码成一个向量,称为隐藏状态向量。

在该类的初始化方法中,有如下3个主要的成员变量。

❑self.hidden_size代表了隐藏状态的维度。

❑self.embedding是一个嵌入层,用于将输入序列的每个元素映射成对应的稠密向量表示。

❑self.rnn是一个循环神经网络层,其输入和输出的维度都是hidden_size,并且采用batch_first=True的方式进行设置。

在前向传播过程中,首先,将输入序列进行嵌入操作并使用dropout进行随机失活处理,得到嵌入后的序列。然后,将嵌入后的序列输入到RNN层中进行编码操作。最后,返回编码后的输出序列以及最终的隐藏(hidden)状态作为输出。

下面我们实例化一个编码器对象,然后使用玩具数据查看输入向量、输出向量和隐藏状态的维度。

python

encoder=EncoderRNN(input_size=10, hidden_size=5) #词表大小为10,隐藏状态维度为5

input_vector=th.tensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])

output, hidden=encoder(input_vector)

print("输入向量的维度:", input_vector.size()) #输入向量的维度: torch.Size([1, 10])

print("输出向量的维度:", output.size())    #输出向量的维度: torch.Size([1, 10, 5])

print("最终隐藏状态的维度:", hidden.size())  #最终隐藏状态的维度: torch.Size([1, 1, 5])

2.解码器

如果Seq2Seq结构的编码器中有多个RNN层,则在解码器中仅使用编码器最后一层的输出,该输出内容也被称为上下文向量,因为它对整个输入序列的上下文进行编码。解码器将使用此上下文向量进行初始化,这其中隐含了一个限定条件,就是解码器的隐藏状态维度和编码器的隐藏状态维度必须相同。解码器最终的输出应该是词元的概率分布,因此在解码器的最后一层使用全连接层进行变换。

解码器在对目标序列的单词进行嵌入之前,需要将整个序列右移一位,然后在序列的开头增加一个标志位SOS_token来表示开始解码。例如,当目标序列是“Hello World!”时,实际输入的应该是[SOS_token, Hello, World, !]。具体的解码过程其实是:输入SOS_token,然后解码器输出Hello,将SOS_token和Hello拼接起来输入解码器,然后输出World,以此类推,直到解码器输出结束词元EOS_token为止。

解码器在前向传播生成结果时,采用了强制学习的方法。此方法是指在训练中使用真实的目标输出作为下一个输入,而不是使用解码器猜测的输出作为下一个输入。例如,在前面这个例子中,当我们输入SOS_token时,期望模型输出Hello,但是如果它实际输出了Hi,那我们会将其先存下来,然后在下一轮输入的时候,将SOS_token和正确答案Hello拼接起来输入解码器。使用强制学习的方法可以加快网络的收敛速度,但当应用训练好的网络时,可能会出现不稳定的情况。

python

class DecoderRNN(nn.Module):

  def __init__(self, hidden_size, output_size):

    super(DecoderRNN, self).__init__()

    self.embedding=nn.Embedding(output_size, hidden_size)

    self.rnn=nn.RNN(hidden_size, hidden_size, batch_first=True)

    self.out=nn.Linear(hidden_size, output_size)


  def forward(self, encoder_outputs, encoder_hidden, target_tensor=None):

    batch_size=encoder_outputs.size(0)

    decoder_input=th.empty(batch_size, 1, dtype=th.long).fill_(SOS_token)  # Start of Sentence词元,表示开始生成一个句子

    decoder_hidden=encoder_hidden

    decoder_outputs=[]

    for i in range(MAX_LENGTH):

      decoder_output, decoder_hidden =self.forward_step(decoder_input, decoder_hidden)

      decoder_outputs.append(decoder_output)

      if target_tensor is not None:  # 强制学习

        decoder_input=target_tensor[:, i].unsqueeze(1)

      else:

        _, topi=decoder_output.topk(1)

      decoder_input=topi.squeeze(-1).detach()


    decoder_outputs=th.cat(decoder_outputs, dim=1)

    decoder_outputs=F.log_softmax(decoder_outputs, dim=-1)

    return decoder_outputs, decoder_hidden, None


  def forward_step(self, x, hidden):

    x=self.embedding(x)

    x=F.relu(x)

    x, hidden=self.rnn(x, hidden)

    output=self.out(x)

    return output, hidden

这段代码定义了一个名为DecoderRNN的类,用于根据编码器的输出和隐藏状态逐步生成目标序列。

在该类的初始化方法中,有三个主要的成员变量。

其中self.embedding和self.rnn前面已经介绍过,不再赘述。self.out是一个线性层,用于将RNN的输出映射到目标序列的维度。

在前向传播过程中,首先根据编码器的输出和隐藏状态初始化解码器的第一个输入,然后进入一个循环,循环次数为最大序列长度(MAX_LENGTH)。在每一次循环中,调用forward_step方法来逐步生成目标序列。强制学习方法会将目标序列作为下一步的输入。循环过程中的每一个输出都被存储在decoder_outputs列表中。

最后,在前向传播的末尾,将存储的所有输出进行拼接,然后经过log_softmax函数进行标准化,得到最终的decoder_outputs。同时,返回最终的隐藏状态(decoder_hidden)以及“None”以保持在训练循环中的一致性。

forward_step方法用于执行每一步的解码过程。它首先对输入序列进行嵌入操作,然后通过ReLU激活函数激活,接着经过RNN层进行解码计算,再通过线性层映射到目标序列的维度。最后,返回解码结果和隐藏状态。

下面我们实例化一个解码器对象,然后使用玩具数据看一下最终输出向量的维度。

python

decoder=DecoderRNN(hidden_size=5, output_size=10)

target_vector=th.tensor([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])

encoder_outputs, encoder_hidden=encoder(input_vector)

output, hidden, _=decoder(encoder_outputs, encoder_hidden, input_vector)

print("输出向量的维度:", output.size()) #输出向量的维度:[1, 10, 10]

最终输出向量的第一个维度表示批量大小,第二个维度表示最大输出长度,即MAX_LENGTH,第三个维度表示词表大小。