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

1.1.4 实战:日期转换

本小节我们将通过一个非常简单、直观的日期转换的例子,来更加深入地了解Seq2Seq结构的模型。我们需要实现的功能就是将中文的“年-月-日”格式的日期转换成英文的“day/month/year”(即DD/MM/YYYY)格式的日期,数据的区间是1950~2050年。当然,这个功能比较简单,直接通过一些规则就可以完成映射,为了增加难度,提供的中文日期采用“YY-MM-DD”的格式,即年份数字缺少前两位,模型需要推理转换的日期到底是20世纪还是21世纪。

1.加载数据

训练模型首先需要数据集,由于数据比较简单,可以直接通过规则进行创建。我们需要对每个数字、符号和单词创建一个唯一的索引,以便稍后用作编码器的输入转换和解码器的输出转换。我们可以通过两个字典word2index(word→index)和index2word(index→word)来实现,它们分别表示将词元转换为索引和将索引转换为词元。

python

class DateDataset(Dataset):

  def __init__(self, n):

    # 初始化两个空列表,用于存储中文和英文日期

    self.date_cn=[]

    self.date_en=[]

    for _ in range(n):

      #随机生成年、月和日

      year=random.randint(1950, 2050)

      month=random.randint(1, 12)

      day=random.randint(1, 28) #假设最大为28日

      date=datetime.date(year, month, day)

      # 格式化日期并添加到对应的列表中

      self.date_cn.append(date.strftime("%y-%m-%d"))

      self.date_en.append(date.strftime("%d/%b/%Y"))

    # 创建一个词汇集,包含0~9的数字、-、/和英文日期中的月份缩写

    self.vocab=set([str(i) for i in range(0, 10)]+

             ["-", "/"]+[i.split("/")[1] for i in self.date_en])

    # 创建一个词汇到索引的映射,其中<SOS>、<EOS>和<PAD>分别对应开始、结束和填充标记

    self.word2index={v: i for i, v in enumerate(

      sorted(list(self.vocab)), start=2)}

    self.word2index["<SOS>"]=SOS_token

    self.word2index["<EOS>"]=EOS_token

    self.word2index["<PAD>"]=PAD_token

    # 将开始、结束和填充标记添加到词汇集中

    self.vocab.add("<SOS>")

    self.vocab.add("<EOS>")

    self.vocab.add("<PAD>")

    # 创建一个索引到词汇的映射

    self.index2word={i: v for v, i in self.word2index.items()}

    # 初始化输入和目标列表

    self.input, self.target=[], []

    for cn, en in zip(self.date_cn, self.date_en):

      # 将日期字符串转换为词汇索引列表,然后添加到输入和目标列表中

      self.input.append([self.word2index[v] for v in cn])

      self.target.append(

        [self.word2index["<SOS>"], ]+

        [self.word2index[v] for v in en[:3]]+

        [self.word2index[en[3:6]]]+

        [self.word2index[v] for v in en[6:]]+

        [self.word2index["<EOS>"], ]

      )

    # 将输入和目标列表转换为NumPy数组

    self.input, self.target=np.array(self.input), np.array(self.target)


  def __len__(self):

    # 返回数据集的长度,即输入的数量

    return len(self.input)


  def __getitem__(self, index):

    # 返回给定索引的输入、目标和目标的长度

    return self.input[index], self.target[index], len(self.target[index])


  @property

  def num_word(self):

    # 返回词表的大小

    return len(self.vocab)

这段代码定义了一个名为DateDataset的类,它是一个继承自torch.utils.data.Dataset的自定义数据集类。该数据集用于生成随机的日期数据,并进行数据预处理和编码。

在类的初始化方法中,首先定义了空的date_cn和date_en列表,分别用于存储“YY-MM-DD”类型的日期和“DD/MM/YYYY”类型的日期,然后根据指定的数据数量n生成n个随机的日期数据。每个日期数据都是随机生成的年、月、日,并使用strftime方法将其转换为指定的日期字符串格式并添加到date_cn和date_en列表中。

然后,根据生成的日期数据构建了一个词表(vocab),该词表包含所有在日期数据中出现的数字、分隔符(“-”和“/”)以及月份的缩写。之后,将词表中的每个词和对应的索引构建成字典(word2index和index2word),并添加特殊的标记词(<SOS>、<EOS>)和对应的索引值。

接下来,根据构建的字典,对date_cn和date_en中的每个日期数据进行编码处理。对于中文日期数据(date_cn),将每个字符根据字典转换为对应的索引值,并存储到input列表中。对于英文日期数据(date_en),首先转换为规定的格式,然后在前后拼接上<SOS>和<EOS>,并存储到target列表中。最后,将input和target转换为NumPy数组,并分别存储到self.input和self.target中。

该类还实现了__len__方法和__getitem__方法,它们分别用于返回数据集的长度和指定索引位置的数据样本。另外,该类还定义了一个名为num_word的属性方法,用于返回词表中的词汇数量。

我们可以通过以下语句大致了解该数据集,如图1-7所示。

图1-7 数据集样例示意

2.训练模型

数据准备完成后就可以开始训练模型了,如以下代码所示。

python

n_epochs=100

batch_size=32

MAX_LENGTH=11

hidden_size=128

learning_rate=0.001

dataloader=DataLoader(dataset, batch_size=batch_size, shuffle=True, drop_last=True)

encoder=EncoderRNN(dataset.num_word, hidden_size)

decoder=AttentionDecoderRNN(hidden_size, dataset.num_word)

encoder_optimizer=optim.Adam(encoder.parameters(), lr=learning_rate)

decoder_optimizer=optim.Adam(decoder.parameters(), lr=learning_rate)

criterion=nn.NLLLoss()


for i in range(n_epochs+1):

  total_loss=0

  for input_tensor, target_tensor, target_length in dataloader:

    encoder_optimizer.zero_grad()

    decoder_optimizer.zero_grad()

    encoder_outputs, encoder_hidden=encoder(input_tensor)

    decoder_outputs, _, _=decoder(encoder_outputs, encoder_hidden, target_tensor)

    loss=criterion(

      decoder_outputs.view(-1, decoder_outputs.size(-1)),

      target_tensor.view(-1).long()

    )

    loss.backward()

    encoder_optimizer.step()

    decoder_optimizer.step()

    total_loss+=loss.item()

  total_loss/=len(dataloader)

  if i % 10==0:

    print(f"epoch: {i}, loss: {total_loss}")

首先,我们定义了一些超参数,包括训练的总轮数(n_epochs)、批量大小(batch_size)、最大序列长度(MAX_LENGTH)、隐藏层的维度(hidden_size)、学习率(learning_rate)等。接下来,通过DataLoader加载数据集(dataset),将数据集按照指定的批量大小进行划分,并打乱顺序(shuffle=True)。然后,创建了一个EncoderRNN对象和一个AttentionDecoderRNN对象,并分别使用Adam优化器对它们的参数进行优化。接着,定义了一个损失函数(nn.NLLLoss),用于计算模型输出和目标张量之间的负对数似然损失。

在训练循环中,使用一个外部循环控制训练轮数。在每一轮训练中,使用一个内部循环遍历数据集中的每个批量。首先,将优化器的梯度置为0,以避免累积梯度影响优化的效果。然后,将输入序列(input_tensor)输入Encoder模型,获取Encoder的输出和隐藏状态。再将Encoder的输出和隐藏状态以及目标序列(target_tensor)输入Decoder模型,获取Decoder的输出。计算模型输出和目标序列之间的损失,并执行反向传播和优化器参数更新的操作。累加每个批量的损失以得到总损失。

最后,在每一轮训练结束后,将总损失除以数据加载器(dataloader)中的批量数,得到平均损失。并且,每迭代10次输出一次当前轮数和平均损失。

3.评估

在开始评估模型之前,需要先将编码器和解码器模型设置为评估模式。评估过程与训练过程基本相同,但是没有目标输出,因此我们将解码器的预测结果反馈给自身进行下一步操作。每次预测出一个新的单词后,我们就将其添加到输出字符串中,如果预测到了EOS标记,就停止预测。

python

def evaluate(encoder, decoder, x):

  encoder.eval()

  decoder.eval()

  encoder_outputs, encoder_hidden=encoder(th.tensor(np.array([x])))

  start=th.ones(x.shape[0],1)  # [n, 1]

  start[:,0]=th.tensor(SOS_token).long()

  decoder_outputs, _, _=decoder(encoder_outputs, encoder_hidden)

  _, topi=decoder_outputs.topk(1)

  decoded_ids=topi.squeeze()

  decoded_words=[]

  for idx in decoded_ids:

    decoded_words.append(dataset.index2word[idx.item()])

  return ''.join(decoded_words)


for i in range(5):

  predict=evaluate(encoder, decoder, dataset[i][0])

  print(f"input: {dataset.date_cn[i]}, target: {dataset.date_en[i]}, predict: {predict}")

在上述代码中,通过循环调用evaluate函数来进行预测,并输出预测结果。首先,令输入序列x通过Encoder模型,得到Encoder的输出和隐藏状态。接下来,创建一个初始输入start,其维度为[n, 1],并将其每个元素设置为SOS_token(起始标记)的索引。将Encoder的输出和隐藏状态以及start作为输入,通过Decoder模型得到Decoder的输出。

然后,从Decoder的输出中取出每个位置上的最大值索引(topi),构建出预测的序列(decoded_ids)。接着,根据词典(dataset.index2word)将每个索引转换为对应的词,并存储到decoded_words列表中。最后,将decoded_words中的词按顺序连接起来,得到最终的预测结果(predict)。

如图1-8所示,在主程序中,循环遍历5个数据样本。对于每个样本,调用evaluate函数进行预测,同时输出输入序列(dataset.date_cn[i])、目标序列(dataset.date_en[i])和预测结果(predict)。可以发现,模型不但能够正确地对日期进行转换,而且对于2011年11月28日,也没有将其错误地预测为1911年的日期。

图1-8 Seq2Seq的日期转换模型训练效果示意