找回密码
 立即注册
首页 业界区 业界 大模型基础补全计划(五)---seq2seq实例与测试(编码器、 ...

大模型基础补全计划(五)---seq2seq实例与测试(编码器、解码器架构)

咫噎 4 小时前
PS:要转载请注明出处,本人版权所有。

PS: 这个只是基于《我自己》的理解,

如果和你的原则及想法相冲突,请谅解,勿喷。

环境说明

  无
前言

      本文是这个系列第五篇,它们是:

  • 《大模型基础补全计划(一)---重温一些深度学习相关的数学知识》 https://www.cnblogs.com/Iflyinsky/p/18717317
  • 《大模型基础补全计划(二)---词嵌入(word embedding) 》 https://www.cnblogs.com/Iflyinsky/p/18775451
  • 《大模型基础补全计划(三)---RNN实例与测试》 https://www.cnblogs.com/Iflyinsky/p/18967569
  • 《大模型基础补全计划(四)---LSTM的实例与测试(RNN的改进)》 https://www.cnblogs.com/Iflyinsky/p/19091089
  本文,我们先简单介绍一下编码器-解码器架构,然后介绍一个基于这种架构的机翻模型seq2seq的简单实例。




编码器-解码器(encoder-decoder)架构

   前面的文章中我们的模型示例都是根据已有的文字序列,续写N个字。在自然语言处理中,还有有一类需求也是比较经典,那就是机器翻译。
   对于机器翻译来说,其核心就是将一种语言翻译为另外一种语言,换句话说就是一种序列数据到另外一种序列数据。从这里来看,出现了两种序列数据,那么必然的很容易想到类似两个RNN的独立网络来处理这种任务,基于这种情况,有人提出了编码器-解码器架构,下图是这种架构的示意图。
            
1.png
         从示意图可知,这种架构的核心就是处理输入序列,得到中间状态,将中间状态传给解码器,解码器负责生成输出序列。对于翻译任务来说,输入序列就是原文,输出序列就是译文。
  这里说起来还是概念性的,我们下面从一个经典的编码器、解码器结构的模型来实际 演示一下翻译需求的模型是什么样子的。




基于 seq2seq 的  英文翻译中文  的实例

  


英文中文翻译数据集

   首先数据集下载地址是http://www.manythings.org/anki/ 中的cmn-eng.zip 文件,其内部的数据集格式大概如下:
  1.     I try.        我试试。        CC-BY 2.0 (France) Attribution: tatoeba.org #20776 (CK) & #8870261 (will66)
  2.     I won!        我赢了。        CC-BY 2.0 (France) Attribution: tatoeba.org #2005192 (CK) & #5102367 (mirrorvan)
  3.     Oh no!        不会吧。        CC-BY 2.0 (France) Attribution: tatoeba.org #1299275 (CK) & #5092475 (mirrorvan)
  4.     Cheers!        乾杯!        CC-BY 2.0 (France) Attribution: tatoeba.org #487006 (human600) & #765577 (Martha)
  5.     Got it?        知道了没有?        CC-BY 2.0 (France) Attribution: tatoeba.org #455353 (CM) & #455357 (GlossaMatik)
  6.     Got it?        懂了吗?        CC-BY 2.0 (France) Attribution: tatoeba.org #455353 (CM) & #2032276 (ydcok)
  7.     Got it?        你懂了吗?        CC-BY 2.0 (France) Attribution: tatoeba.org #455353 (CM) & #7768205 (jiangche)
  8.     He ran.        他跑了。        CC-BY 2.0 (France) Attribution: tatoeba.org #672229 (CK) & #5092389 (mirrorvan)
复制代码
   由于我的卡(3060 12G)有点拉库,为了效率,因此整个数据集只用前面2千条即可。




文本预处理
  1. # dataset.py
  2. import collections
  3. import torch
  4. from torch.utils import data
  5. # 下面返回的数据是:
  6. # [['Hi.', '嗨。'], ['Hi.', '你好。'], ['Run.', '你用跑的。'], ['Stop!', '住手!'], ['Wait!', '等等!'], ... ...]
  7. def read_data():
  8.     with open('cmn-eng/cmn.txt', 'r',
  9.              encoding='utf-8') as f:
  10.         lines = f.readlines()
  11.    
  12.     return [line.split("        ")[:2] for line in lines]
  13.    
  14. # 输出是:
  15. # [['Hi.'], ['Hi.'], ['Run.'], ['Stop!'], ['Wait!']]
  16. # [['嗨', '。'], ['你', '好', '。'], ['你', '用', '跑', '的', '。'], ['住', '手', '!'], ['等', '等', '!']]
  17. # ['Hi.', 'Hi.', 'Run.', 'Stop!', 'Wait!']
  18. # ['嗨。', '你好。', '你用跑的。', '住手!', '等等!']
  19. def tokenize(lines, token='char'):  #@save
  20.     """将文本行拆分为单词或字符词元"""
  21.     source_tokenize, target_tokenize = [], []
  22.     source_line, target_line = [], []
  23.     print(f'dataset len = {len(lines)}')
  24.     for line in lines:
  25.         s = line[0]
  26.         t = line[1]
  27.         source_line.append(s)
  28.         target_line.append(t)
  29.         source_tokenize.append(s.split(' '))
  30.         target_tokenize.append([word for word in t])
  31.     return source_tokenize, target_tokenize, source_line, target_line
  32. # 词元的类型是字符串,而模型需要的输入是数字,因此这种类型不方便模型使用。 现在,让我们构建一个字典,
  33. # 通常也叫做词表(vocabulary), 用来将字符串类型的词元映射到从开始的数字索引中。
  34. def count_corpus(tokens):  #@save
  35.     """统计词元的频率"""
  36.     # 这里的tokens是1D列表或2D列表
  37.     if len(tokens) == 0 or isinstance(tokens[0], list):
  38.         # 将词元列表展平成一个列表
  39.         tokens = [token for line in tokens for token in line]
  40.     return collections.Counter(tokens)
  41. # 返回类似{'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1}的一个字典
  42. class Vocab:
  43.     """文本词表"""
  44.     def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
  45.         if tokens is None:
  46.             tokens = []
  47.         if reserved_tokens is None:
  48.             reserved_tokens = []
  49.         # 按出现频率排序
  50.         # 对于Counter("hello world"),结果如下
  51.         # Counter({'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1})
  52.         counter = count_corpus(tokens)
  53.         self._token_freqs = sorted(counter.items(), key=lambda x: x[1],
  54.                                    reverse=True)
  55.         # 未知词元的索引为0
  56.         self.idx_to_token = ['<unk>'] + reserved_tokens
  57.         self.token_to_idx = {token: idx
  58.                              for idx, token in enumerate(self.idx_to_token)}
  59.         for token, freq in self._token_freqs:
  60.             if freq < min_freq:
  61.                 break
  62.             if token not in self.token_to_idx:
  63.                 self.idx_to_token.append(token)
  64.                 self.token_to_idx[token] = len(self.idx_to_token) - 1
  65.     def __len__(self):
  66.         return len(self.idx_to_token)
  67.     def __getitem__(self, tokens):
  68.         if not isinstance(tokens, (list, tuple)):
  69.             return self.token_to_idx.get(tokens, self.unk)
  70.         return [self.__getitem__(token) for token in tokens]
  71.     def to_tokens(self, indices):
  72.         if not isinstance(indices, (list, tuple)):
  73.             return self.idx_to_token[indices]
  74.         return [self.idx_to_token[index] for index in indices]
  75.     @property
  76.     def unk(self):  # 未知词元的索引为0
  77.         return 0
  78.     @property
  79.     def token_freqs(self):
  80.         return self._token_freqs   
  81.    
  82. def truncate_pad(line, num_steps, padding_token):
  83.     """截断或填充文本序列"""
  84.     if len(line) > num_steps:
  85.         return line[:num_steps]  # 截断
  86.     return line + [padding_token] * (num_steps - len(line))  # 填充
  87. def build_array(lines, vocab, num_steps):
  88.     """将机器翻译的文本序列转换成小批量"""
  89.     lines = [vocab[l] for l in lines] # 每行的token转换为其id
  90.     lines = [l + [vocab['<eos>']] for l in lines] # 每行的token后加上eos的id
  91.     array = torch.tensor([truncate_pad(
  92.         l, num_steps, vocab['<pad>']) for l in lines])
  93.     valid_len = (array != vocab['<pad>']).type(torch.int32).sum(1)
  94.     return array, valid_len
  95. def load_array(data_arrays, batch_size, is_train=True):
  96.     """构造一个PyTorch数据迭代器
  97.     Defined in :numref:`sec_linear_concise`"""
  98.     dataset = data.TensorDataset(*data_arrays)
  99.     return data.DataLoader(dataset, batch_size, shuffle=is_train)
  100. def load_data(batch_size, num_steps, num_examples=600):
  101.     """返回翻译数据集的迭代器和词表"""
  102.     text = read_data()
  103.     source, target, src_line, tgt_line = tokenize(text)
  104.     # 返回类似{'l': 3, 'o': 2, 'h': 1, 'e': 1, ' ': 1, 'w': 1, 'r': 1, 'd': 1}的一个字典
  105.     src_vocab = Vocab(source, min_freq=0,
  106.                           reserved_tokens=['<pad>', '<bos>', '<eos>'])
  107.     tgt_vocab = Vocab(target, min_freq=0,
  108.                           reserved_tokens=['<pad>', '<bos>', '<eos>'])
  109.     # 首先把每行的词转换为了其对应的id,然后给每一行的末尾添加token <eos>, 然后根据num_steps,如果line长度不足,补<pad>,如果长度超出,截断
  110.     # 一种类型的输出是:
  111.     # [
  112.     #     [line0-char0-id, line0-char1-id, line0-char2-id, ...., eos-id],
  113.     #     [line1-char0-id, line1-char1-id, line1-char2-id, ...., eos-id], 注意,最后的末尾可能没有eos
  114.     #     .....
  115.     # ]
  116.     src_array, src_valid_len = build_array(source, src_vocab, num_steps)
  117.     tgt_array, tgt_valid_len = build_array(target, tgt_vocab, num_steps)
  118.     data_arrays = (src_array, src_valid_len, tgt_array, tgt_valid_len)
  119.     data_iter = load_array(data_arrays, batch_size)
  120.     return data_iter, src_vocab, tgt_vocab, src_line, tgt_line
复制代码
  上面代码做了如下事情:

  • 根据数据集的格式,读取每一行,只提前每行前面2个字符串。
  • 然后我们对每一行进行文字切割,得到了一个二维列表,列表中的每一行又被分割为一个个中文文字和一个个英文的词,也就得到了一个个token。(特别注意,站在当前的时刻,这里的token和现在主流的大语言模型的token概念是一样的,但是不是一样的实现。)
  • 由于模型不能直接处理文字,我们需要将文字转换为数字,那么直接的做法就是将一个个token编号即可,这个时候我们得到了词表(vocabulary)。
  • 然后我们根据我们得到的词表,对原始数据集进行数字化,得到一个列表,列表中每个元素就是一个个token对应的索引。
  • 最后得到:基于pytorch的DataLoader、原文词表、译文词表、原文文字列表、译文文字列表
  此外,在这里出现了几个在后面的大语言模型中也会出现的词:BOS/EOS。这两个分别代表一次对话的起始、结尾,这里直接记住就行。


搭建seq2seq训练框架

  首先引用一些包和一些辅助class
  1. import os
  2. import random
  3. import torch
  4. import math
  5. from torch import nn
  6. from torch.nn import functional as F
  7. import numpy as np
  8. import time
  9. import visdom
  10. import collections
  11. import dataset
  12. class Accumulator:
  13.     """在n个变量上累加"""
  14.     def __init__(self, n):
  15.         """Defined in :numref:`sec_softmax_scratch`"""
  16.         self.data = [0.0] * n
  17.     def add(self, *args):
  18.         self.data = [a + float(b) for a, b in zip(self.data, args)]
  19.     def reset(self):
  20.         self.data = [0.0] * len(self.data)
  21.     def __getitem__(self, idx):
  22.         return self.data[idx]
  23.    
  24. class Timer:
  25.     """记录多次运行时间"""
  26.     def __init__(self):
  27.         """Defined in :numref:`subsec_linear_model`"""
  28.         self.times = []
  29.         self.start()
  30.     def start(self):
  31.         """启动计时器"""
  32.         self.tik = time.time()
  33.     def stop(self):
  34.         """停止计时器并将时间记录在列表中"""
  35.         self.times.append(time.time() - self.tik)
  36.         return self.times[-1]
  37.     def avg(self):
  38.         """返回平均时间"""
  39.         return sum(self.times) / len(self.times)
  40.     def sum(self):
  41.         """返回时间总和"""
  42.         return sum(self.times)
  43.     def cumsum(self):
  44.         """返回累计时间"""
  45.         return np.array(self.times).cumsum().tolist()
  46.    
复制代码
  然后我们根据编码器、解码器架构,设计seq2seq的网络主干
  1. class Encoder(nn.Module):
  2.     """编码器-解码器架构的基本编码器接口"""
  3.     def __init__(self, **kwargs):
  4.         # 调用父类nn.Module的构造函数,确保正确初始化
  5.         super(Encoder, self).__init__(**kwargs)
  6.     def forward(self, X, *args):
  7.         # 抛出未实现错误,意味着该方法需要在子类中具体实现
  8.         raise NotImplementedError
  9. class Decoder(nn.Module):
  10.     """编码器-解码器架构的基本解码器接口
  11.     Defined in :numref:`sec_encoder-decoder`"""
  12.     def __init__(self, **kwargs):
  13.         # 调用父类nn.Module的构造函数,确保正确初始化
  14.         super(Decoder, self).__init__(**kwargs)
  15.     def init_state(self, enc_outputs, *args):
  16.         # 抛出未实现错误,意味着该方法需要在子类中具体实现
  17.         raise NotImplementedError
  18.     def forward(self, X, state):
  19.         # 抛出未实现错误,意味着该方法需要在子类中具体实现
  20.         raise NotImplementedError
  21. class EncoderDecoder(nn.Module):
  22.     """编码器-解码器架构的基类
  23.     Defined in :numref:`sec_encoder-decoder`"""
  24.     def __init__(self, encoder, decoder, **kwargs):
  25.         # 调用父类nn.Module的构造函数,确保正确初始化
  26.         super(EncoderDecoder, self).__init__(**kwargs)
  27.         # 将传入的编码器实例赋值给类的属性
  28.         self.encoder = encoder
  29.         # 将传入的解码器实例赋值给类的属性
  30.         self.decoder = decoder
  31.     def forward(self, enc_X, dec_X, *args):
  32.         # 调用编码器的前向传播方法,处理输入的编码器输入数据enc_X
  33.         enc_outputs = self.encoder(enc_X, *args)
  34.         # 调用解码器的init_state方法,根据编码器的输出初始化解码器的状态
  35.         dec_state = self.decoder.init_state(enc_outputs, *args)
  36.         # 调用解码器的前向传播方法,处理输入的解码器输入数据dec_X和初始化后的状态
  37.         return self.decoder(dec_X, dec_state)
  38.    
  39. #@save
  40. class Seq2SeqEncoder(Encoder):
  41.     """用于序列到序列学习的循环神经网络编码器"""
  42.     def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
  43.                  dropout=0, **kwargs):
  44.         super(Seq2SeqEncoder, self).__init__(**kwargs)
  45.         # 嵌入层
  46.         self.embedding = nn.Embedding(vocab_size, embed_size)
  47.         self.rnn = nn.GRU(embed_size, num_hiddens, num_layers,
  48.                           dropout=dropout)
  49.         # self.lstm = nn.LSTM(embed_size, num_hiddens, num_layers)
  50.     def forward(self, X, *args):
  51.         # 输入X.shape = (batch_size,num_steps)
  52.         # 输出'X'的形状:(batch_size,num_steps,embed_size)
  53.         X = self.embedding(X)
  54.         # 在循环神经网络模型中,第一个轴对应于时间步
  55.         X = X.permute(1, 0, 2)
  56.         # 如果未提及状态,则默认为0
  57.         output, state = self.rnn(X)
  58.         # output : 这个返回值是所有时间步的隐藏状态序列
  59.         # output的形状:(num_steps,batch_size,num_hiddens)
  60.         # hn (hidden) : 这是每一层rnn的最后一个时间步的隐藏状态
  61.         # state的形状:(num_layers,batch_size,num_hiddens)
  62.         return output, state
  63. class Seq2SeqDecoder(Decoder):
  64.     """用于序列到序列学习的循环神经网络解码器"""
  65.     def __init__(self, vocab_size, embed_size, num_hiddens, num_layers,
  66.                  dropout=0, **kwargs):
  67.         super(Seq2SeqDecoder, self).__init__(**kwargs)
  68.         self.embedding = nn.Embedding(vocab_size, embed_size)
  69.         self.rnn = nn.GRU(embed_size + num_hiddens, num_hiddens, num_layers,
  70.                           dropout=dropout)
  71.         self.dense = nn.Linear(num_hiddens, vocab_size)
  72.     def init_state(self, enc_outputs, *args):
  73.         return enc_outputs[1]
  74.     def forward(self, X, state):
  75.         # 输出'X'的形状:(batch_size,num_steps,embed_size)
  76.         X = self.embedding(X).permute(1, 0, 2)
  77.         # 广播context,使其具有与X相同的num_steps
  78.         context = state[-1].repeat(X.shape[0], 1, 1)
  79.         X_and_context = torch.cat((X, context), 2)
  80.         output, state = self.rnn(X_and_context, state)
  81.         output = self.dense(output).permute(1, 0, 2)
  82.         # output的形状:(batch_size,num_steps,vocab_size)
  83.         # state的形状:(num_layers,batch_size,num_hiddens)
  84.         return output, state
复制代码
  我们结合上面的架构图对比着看,首先声明一下decoder/encoder的接口类:

  • 声明了Encoder(nn.Module),Encoder(nn.Module)其输入是原文,输出是中间状态。
  • 声明了Decoder(nn.Module),Decoder(nn.Module)的输入是BOS和中间状态,输出是译文。
  • 声明了EncoderDecoder(nn.Module)类,串联Encoder(nn.Module)/Decoder(nn.Module)进行运行。
  然后声明实际的Seq2SeqEncoder部分:

  • 声明了Seq2SeqEncoder(Encoder),其是seq2seq编码器部分的实际定义,其输入是一串原文,然后经过了nn.Embedding,将输入的token序列转换为token-embedding,然后送入nn.GRU,得到了两个值:最后一层rnn的所有时间步的隐藏状态output(shape=num_steps,batch_size,num_hiddens),所有层rnn的最后一个时间步的隐藏状态h_n(shape=num_layers,batch_size,num_hiddens)
  • 从Seq2SeqEncoder(Encoder)上面的分析可知:rnn的输出output代表的是每一个时间步,当前序列的总结信息,h_n encoder的隐藏态参数。
  最后声明实际的Seq2SeqDecoder部分:

  • 声明了Seq2SeqDecoder(Decoder),输入是:一个是bos,一个是Seq2SeqEncoder(Encoder)输出的隐藏态state(output,h_n)。首先将bos转换为embedding向量,然后将h_n的最后一个数据(也就是原文的总结,rnn最后一层最后一个时间步的隐藏态)和embedding组合在一起(注意:这里已经将原文的语义已经和bos输入混合在一起了),和Seq2SeqEncoder(Encoder) state作为的隐藏状态初始值,一起传入rnn,然后经过nn.Linear的映射,得到了decoder的输出。
  • 从Seq2SeqDecoder(Decoder)的分析可知,经过了nn.Linear映射之后,我们将decoder层的rnn的output转换为词表大小的一个向量,这个向量我们可以看做下一个字的分数Logits(注意:这个概念在后续大语言模型中,有比较大的作用)。
  这里,nn.RNN等pytorch层的输出,可以结合下面这个图来理解(图来自于参考文献相关链接):
            
2.png
          下面给出的就是训练、预测部分的代码:
  1. def try_gpu(i=0):
  2.     """如果存在,则返回gpu(i),否则返回cpu()
  3.     Defined in :numref:`sec_use_gpu`"""
  4.     if torch.cuda.device_count() >= i + 1:
  5.         return torch.device(f'cuda:{i}')
  6.     return torch.device('cpu')
  7. def sequence_mask(X, valid_len, value=0):
  8.     """在序列中屏蔽不相关的项"""
  9.     maxlen = X.size(1)
  10.     mask = torch.arange((maxlen), dtype=torch.float32,
  11.                         device=X.device)[None, :] < valid_len[:, None]
  12.     X[~mask] = value
  13.     return X
  14. class MaskedSoftmaxCELoss(nn.CrossEntropyLoss):
  15.     """带遮蔽的softmax交叉熵损失函数"""
  16.     # pred的形状:(batch_size,num_steps,vocab_size)
  17.     # label的形状:(batch_size,num_steps)
  18.     # valid_len的形状:(batch_size,)
  19.     def forward(self, pred, label, valid_len):
  20.         weights = torch.ones_like(label)
  21.         weights = sequence_mask(weights, valid_len)
  22.         self.reduction='none'
  23.         unweighted_loss = super(MaskedSoftmaxCELoss, self).forward(
  24.             pred.permute(0, 2, 1), label)
  25.         weighted_loss = (unweighted_loss * weights).mean(dim=1)
  26.         return weighted_loss
  27.    
  28. def grad_clipping(net, theta):  #@save
  29.     """裁剪梯度"""
  30.     if isinstance(net, nn.Module):
  31.         params = [p for p in net.parameters() if p.requires_grad]
  32.     else:
  33.         params = net.params
  34.     norm = torch.sqrt(sum(torch.sum((p.grad ** 2)) for p in params))
  35.     if norm > theta:
  36.         for param in params:
  37.             param.grad[:] *= theta / norm
  38. def train_seq2seq(net, data_iter, lr, num_epochs, tgt_vocab, device):
  39.     """训练序列到序列模型"""
  40.     def xavier_init_weights(m):
  41.         if type(m) == nn.Linear:
  42.             nn.init.xavier_uniform_(m.weight)
  43.         if type(m) == nn.GRU:
  44.             for param in m._flat_weights_names:
  45.                 if "weight" in param:
  46.                     nn.init.xavier_uniform_(m._parameters[param])
  47.     net.apply(xavier_init_weights)
  48.     net.to(device)
  49.     optimizer = torch.optim.Adam(net.parameters(), lr=lr)
  50.     loss = MaskedSoftmaxCELoss()
  51.     net.train()
  52.     vis = visdom.Visdom(env=u'test1', server="http://127.0.0.1", port=8097)
  53.     animator = vis
  54.     for epoch in range(num_epochs):
  55.         timer = Timer()
  56.         metric = Accumulator(2)  # 训练损失总和,词元数量
  57.         for batch in data_iter:
  58.             #清零(reset)优化器中的梯度缓存
  59.             optimizer.zero_grad()
  60.             # x.shape = [batch_size, num_steps]
  61.             X, X_valid_len, Y, Y_valid_len = [x.to(device) for x in batch]
  62.             # bos.shape = batch_size 个 bos-id
  63.             bos = torch.tensor([tgt_vocab['<bos>']] * Y.shape[0],
  64.                           device=device).reshape(-1, 1)
  65.             # dec_input.shape = (batch_size, num_steps)
  66.             # 解码器的输入通常由序列的起始标志 bos 和目标序列(去掉末尾的部分 Y[:, :-1])组成。
  67.             dec_input = torch.cat([bos, Y[:, :-1]], 1)  # 强制教学
  68.             # Y_hat的形状:(batch_size,num_steps,vocab_size)
  69.             Y_hat, _ = net(X, dec_input, X_valid_len)
  70.             l = loss(Y_hat, Y, Y_valid_len)
  71.             l.sum().backward()      # 损失函数的标量进行“反向传播”
  72.             grad_clipping(net, 1)
  73.             num_tokens = Y_valid_len.sum()
  74.             optimizer.step()
  75.             with torch.no_grad():
  76.                 metric.add(l.sum(), num_tokens)
  77.         if (epoch + 1) % 10 == 0:
  78.             # print(predict('你是?'))
  79.             # print(epoch)
  80.             # animator.add(epoch + 1, )
  81.             if epoch == 9:
  82.                 # 清空图表:使用空数组来替换现有内容
  83.                 vis.line(X=np.array([0]), Y=np.array([0]), win='train_ch8', update='replace')
  84.             # _loss_val = l
  85.             # _loss_val = _loss_val.cpu().sum().detach().numpy()
  86.             vis.line(
  87.                 X=np.array([epoch + 1]),
  88.                 Y=[ metric[0] / metric[1]],
  89.                 win='train_ch8',
  90.                 update='append',
  91.                 opts={
  92.                     'title': 'train_ch8',
  93.                     'xlabel': 'epoch',
  94.                     'ylabel': 'loss',
  95.                     'linecolor': np.array([[0, 0, 255]]),  # 蓝色线条
  96.                 }
  97.             )
  98.     print(f'loss {metric[0] / metric[1]:.3f}, {metric[1] / timer.stop():.1f} '
  99.         f'tokens/sec on {str(device)}')
  100.     torch.save(net.cpu().state_dict(), 'model_h.pt')  # [[6]]
  101.     torch.save(net.cpu(), 'model.pt')  # [[6]]
  102. def predict_seq2seq(net, src_sentence, src_vocab, tgt_vocab, num_steps,
  103.                     device, save_attention_weights=False):
  104.     """序列到序列模型的预测"""
  105.     # 在预测时将net设置为评估模式
  106.     net.eval()
  107.     src_tokens = src_vocab[src_sentence.lower().split(' ')] + [
  108.         src_vocab['<eos>']]
  109.     enc_valid_len = torch.tensor([len(src_tokens)], device=device)
  110.     src_tokens = dataset.truncate_pad(src_tokens, num_steps, src_vocab['<pad>'])
  111.     # 添加批量轴
  112.     enc_X = torch.unsqueeze(
  113.         torch.tensor(src_tokens, dtype=torch.long, device=device), dim=0)
  114.     enc_outputs = net.encoder(enc_X, enc_valid_len)
  115.     dec_state = net.decoder.init_state(enc_outputs, enc_valid_len)
  116.     # 添加批量轴
  117.     dec_X = torch.unsqueeze(torch.tensor(
  118.         [tgt_vocab['<bos>']], dtype=torch.long, device=device), dim=0)
  119.     output_seq, attention_weight_seq = [], []
  120.     for _ in range(num_steps):
  121.         Y, dec_state = net.decoder(dec_X, dec_state)
  122.         # 我们使用具有预测最高可能性的词元,作为解码器在下一时间步的输入
  123.         dec_X = Y.argmax(dim=2)
  124.         pred = dec_X.squeeze(dim=0).type(torch.int32).item()
  125.         # 保存注意力权重(稍后讨论)
  126.         if save_attention_weights:
  127.             attention_weight_seq.append(net.decoder.attention_weights)
  128.         # 一旦序列结束词元被预测,输出序列的生成就完成了
  129.         if pred == tgt_vocab['<eos>']:
  130.             break
  131.         output_seq.append(pred)
  132.     return ' '.join(tgt_vocab.to_tokens(output_seq)), attention_weight_seq
  133. def bleu(pred_seq, label_seq, k):  #@save
  134.     """计算BLEU"""
  135.     pred_tokens, label_tokens = pred_seq.split(' '), [i for i in label_seq]
  136.     len_pred, len_label = len(pred_tokens), len(label_tokens)
  137.     score = math.exp(min(0, 1 - len_label / len_pred))
  138.     for n in range(1, k + 1):
  139.         num_matches, label_subs = 0, collections.defaultdict(int)
  140.         for i in range(len_label - n + 1):
  141.             label_subs[' '.join(label_tokens[i: i + n])] += 1
  142.         for i in range(len_pred - n + 1):
  143.             if label_subs[' '.join(pred_tokens[i: i + n])] > 0:
  144.                 num_matches += 1
  145.                 label_subs[' '.join(pred_tokens[i: i + n])] -= 1
  146.         score *= math.pow(num_matches / (len_pred - n + 1), math.pow(0.5, n))
  147.     return score
复制代码
  这里首先要介绍一下其损失函数,核心两个:

  • 通过交叉熵计算真实分布、预测分布的差异性,差异性越小,意味着我们的模型越好
  • 由于我们是序列模型,可能涉及pad项,这些pad项的位置是无意义的,但是对模型有影响,我们需要再loss中剔除掉这种无意义的位置,我们用mask来屏蔽。
  然后训练过程的核心就是:从数据集中获取 训练数据、验证数据,通过训练数据得到预测数据,预测数据和验证数据进行loss计算,然后进行反向传播,找到loss最小化的方向,然后最小化loss,模型就会越来越好。
  然后就是介绍预测部分的内容:先将原文输入到seq的encoder,然后将bos序列 + seq的encoder的隐藏态传给seq的decoder,就可以得到下一个字的输出,直到我们遇到eos,预测结束。
  我们虽然预测完毕了,得到了原文对应的译文,但是我们需要一种方法来评估我们翻译的是不是正确,这里用的方法是bleu,它的作用就是评估输出序列与目标序列的精确度。
  最后,我们开始训练过程,注意,下面的例子是先进行训练,然后保存pt模型,然后加载模型进行预测推理。
  1. if __name__ == '__main__':
  2.     embed_size, num_hiddens, num_layers, dropout = 32, 32, 2, 0.1
  3.     batch_size, num_steps = 64, 10
  4.     lr, num_epochs, device = 0.005, 2000, try_gpu()
  5.     # train_iter 每个迭代输出:(batch_size, num_steps)
  6.     train_iter, src_vocab, tgt_vocab, source, target = dataset.load_data(batch_size, num_steps)
  7.     encoder = Seq2SeqEncoder(len(src_vocab), embed_size, num_hiddens, num_layers,
  8.                         dropout)
  9.     decoder = Seq2SeqDecoder(len(tgt_vocab), embed_size, num_hiddens, num_layers,
  10.                             dropout)
  11.     net = EncoderDecoder(encoder, decoder)
  12.     is_train = False
  13.     if is_train:
  14.         train_seq2seq(net, train_iter, lr, num_epochs, tgt_vocab, device)
  15.     else:
  16.         state_dict = torch.load('model_h.pt')
  17.         net.load_state_dict(state_dict)
  18.         net.to(device)
  19.         C = 0
  20.         C1 = 0
  21.         for i in range(2000):
  22.             # print(source[i])
  23.             # print(target[i])
  24.             translation, attention_weight_seq = predict_seq2seq(
  25.                 net, source[i], src_vocab, tgt_vocab, num_steps, device)
  26.             
  27.             score = bleu(translation, target[i], k=2)
  28.             if score > 0.0:
  29.                 C = C + 1
  30.                 if score > 0.8:
  31.                     C1 = C1 + 1
  32.                 print(f'{source[i]} => {translation}, bleu {score:.3f}')
  33.         print(f'Counter(bleu > 0) = {C}')
  34.         print(f'Valid-Counter(bleu > 0.8) = {C1}')
复制代码
           
3.png
                  
4.png
         从上面的图可以看到,这个模型有一定的翻译效果。
  此外,我这里计算了非零的bleu以及大于0.8的bleu的个数,这个个数勉强可以评估,我们对现在这个seq2seq模型优化的效果,为后面的文章提前做一些准备工作。




后记

  本文出现了bos/eos/logits等一些概念的应用,这些应用在大语言模型中也有体现。
  此外,我们从当前的模型结构也可以知道,当前并没有解决输入序列过长时,序列前面部分信息可能丢失,序列中的重点信息没有动态突出的问题。
参考文献


  • https://zh.d2l.ai/chapter_recurrent-modern/encoder-decoder.html
  • https://zh.d2l.ai/chapter_recurrent-modern/seq2seq.html
  • https://www.geeksforgeeks.org/nlp/stacked-rnns-in-nlp/
  • https://docs.pytorch.org/docs/stable/generated/torch.nn.RNN.html


                    打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)               
5.jpeg
    PS: 请尊重原创,不喜勿喷。

PS: 要转载请注明出处,本人版权所有。

PS: 有问题请留言,看到后我会第一时间回复。


来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册