当前位置:   article > 正文

闲聊机器人之Seq2Seq模型的原理_seq2seq模型原理

seq2seq模型原理

闲聊机器人之Seq2Seq模型的原理

闲聊机器人的介绍

在项目准备阶段我们知道,用户说了一句话后,会判断其意图,如果是想进行闲聊,那么就会调用闲聊模型返回结果,这是我们会在项目中实现的功能。

目前市面上的常见闲聊机器人有微软小冰这种类型的模型,很久之前还有小黄鸡这种体验更差的模型

常见的闲聊模型都是一种seq2seq的结构,接下来使用seq2seq来实现我们的闲聊机器人。

Seq2Seq模型的原理

1. Seq2Seq的介绍

Sequence to sequence (seq2seq)是由encoder(编码器)decoder(解码器)两个RNN的组成的。其中encoder负责对输入句子的理解,转化为context vector,decoder负责对理解后的句子的向量进行处理,解码,获得输出。上述的过程和我们大脑理解东西的过程很相似,听到一句话,理解之后,尝试组装答案,进行回答

那么此时,就有一个问题,在encoder的过程中得到的context vector作为decoder的输入,那么这样一个输入,怎么能够得到多个输出呢?

其实就是当前一步的输出,作为下一个单元的输入,然后得到结果

  1. outputs = []
  2. while True:
  3. output = decoderd(output)
  4. outputs.append(output)

那么循环什么时候停止呢?

在训练数据集中,可以再输出的最后面添加一个结束符<END>,如果遇到该结束符,则可以终止循环

  1. outputs = []
  2. while output!="<END>":
  3. output = decoderd(output)
  4. outputs.append(output)

这个结束符只是一个标记,很多人也会使用<EOS>(End Of Sentence)

总之:Seq2seq模型中的encoder接受一个长度为M的序列,得到1个 context vector,之后decoder把这一个context vector转化为长度为N的序列作为输出,从而构成一个M to N的模型,能够处理很多不定长输入输出的问题,比如:文本翻译,问答,文章摘要,关键字写诗等等

 

2. Seq2Seq模型的实现

下面,通过一个简单的列子,来看看普通的Seq2Seq模型应该如何实现。

需求:完成一个模型,实现往模型输入一串数字,输出这串数字+0

例如

  • 输入123456789,输出1234567890

  • 输入52555568,输出525555680

2.1 实现流程

  1. 文本转化为序列(数字序列,torch.LongTensor

  2. 使用序列,准备数据集,准备Dataloader

  3. 完成编码器

  4. 完成解码器

  5. 完成seq2seq模型

  6. 完成模型训练的逻辑,进行训练

  7. 完成模型评估的逻辑,进行模型评估

2.2 文本转化为序列

由于输入的是数字,为了把这些数字和词典中的真实数字进行对应,可以把这些数字理解为字符串

那么我们需要做的就是:

  1. 把字符串对应为数字

  2. 把数字转化为字符串

完成逻辑和之前相同,创建word_sequence.py文件,实现上述逻辑

  1. class NumSequence:
  2. UNK_TAG = "UNK" # 未知词
  3. PAD_TAG = "PAD" # 填充词,实现文本对齐,即一个batch中的句子长度都是相同的,短句子会被padding
  4. EOS_TAG = "EOS" # 句子的开始
  5. SOS_TAG = "SOS" # 句子的结束
  6. UNK = 0
  7. PAD = 1
  8. EOS = 2
  9. SOS = 3
  10. def __init__(self):
  11. self.dict = {
  12. self.UNK_TAG: self.UNK,
  13. self.PAD_TAG: self.PAD,
  14. self.EOS_TAG: self.EOS,
  15. self.SOS_TAG: self.SOS
  16. }
  17. # 得到字符串和数字对应的字典
  18. for i in range(10):
  19. self.dict[str(i)] = len(self.dict) # 随着i的增加,len(dict)++
  20. # 得到数字和字符串对应的字典
  21. self.index2word = dict(zip(self.dict.values(), self.dict.keys()))
  22. def __len__(self):
  23. return len(self.dict)
  24. # 把sentence转化为数字序列
  25. def transform(self, sequence, max_len=None, add_eos=False):
  26. """
  27. sequence:句子
  28. max_len :句子的最大长度
  29. add_eos:是否添加结束符
  30. """
  31. sequence_list = list(str(sequence))
  32. seq_len = len(sequence_list) + 1 if add_eos else len(sequence_list)
  33. if add_eos and max_len is not None:
  34. assert max_len >= seq_len, "max_len 需要大于seq+eos的长度"
  35. _sequence_index = [self.dict.get(i, self.UNK) for i in sequence_list]
  36. if add_eos:
  37. _sequence_index += [self.EOS]
  38. if max_len is not None:
  39. sequence_index = [self.PAD] * max_len
  40. sequence_index[:seq_len] = _sequence_index
  41. return sequence_index
  42. else:
  43. return _sequence_index
  44. # 把序列转回字符串
  45. def inverse_transform(self, sequence_index):
  46. result = []
  47. for i in sequence_index:
  48. if i == self.EOS:
  49. break
  50. result.append(self.index2word.get(int(i), self.UNK_TAG))
  51. return result
  52. # 实例化,供后续调用
  53. num_sequence = NumSequence()
  54. if __name__ == '__main__':
  55. num_sequence = NumSequence()
  56. print(num_sequence.dict)
  57. print(num_sequence.index2word)
  58. print(num_sequence.transform("1231230", add_eos=True))
  59. print(num_sequence.transform("1231230", add_eos=False))
  60. print(num_sequence.transform("1231230AX", add_eos=True))
  61. print(num_sequence.inverse_transform([1, 12, 3]))

运行结果:

 

2.3 准备数据集

2.3.1 准备Dataset  【在样本的target中,需要实现EOS、SOS分别表示句子的开始和结束;在target中需要添加EOS,在transform中需要实现添加EOS的操作】

这里,我们使用随机创建的[0,100000000]的整型,来准备数据集

  1. from torch.utils.data import Dataset, DataLoader
  2. import numpy as np
  3. class RandomDataset(Dataset):
  4. def __init__(self):
  5. super(RandomDataset, self).__init__()
  6. self.total_data_size = 500000
  7. np.random.seed(10)
  8. self.total_data = np.random.randint(1, 100000000, size=[self.total_data_size])
  9. def __getitem__(self, idx):
  10. """返回input,target,input_length,target_length(真实长度)"""
  11. input = str(self.total_data[idx])
  12. return input, input + "0", len(input), len(input) + 1
  13. def __len__(self):
  14. return self.total_data_size

通过随机数的结果,可以看到,大部分的数字长度为8,在目标值后面添加上0和EOS之后,最大长度为10

所以常见config配置文件,添加上max_len:文本最大长度,方便后续的修改

2.3.2 准备DataLoader

在准备DataLoader的过程中,可以通过定义的collate_fn来实现对dataset中batch数据的处理

其中需要注意:

  1. 需要对batch中的数据进行排序,根据数据的真实长度进行降序排序(后面需要用到)

  2. 需要调用文本序列化的方法,把文本进行序列化的操作,同时target需要进行add eos的操作

  3. 最后返回序列的LongTensor格式

  4. DataLoader中有drop_last参数,当数据量无法被batch_size整除时,最后一个batch的数据个数和之前的数据个数长度不同,可以考虑进行删除

  1. def collate_fn(batch):
  2. # 1. 对batch进行排序,按照长度从长到短的顺序排序
  3. batch = sorted(batch, key=lambda x: x[3], reverse=True)
  4. input, target, input_length, target_length = zip(*batch)
  5. # 2.进行padding的操作
  6. input = torch.LongTensor([num_sequence.transform(i, max_len=config.max_len) for i in input])
  7. target = torch.LongTensor([num_sequence.transform(i, max_len=config.max_len, add_eos=True) for i in target])
  8. input_length = torch.LongTensor(input_length)
  9. target_length = torch.LongTensor(target_length)
  10. return input, target, input_length, target_length
  11. data_loader = DataLoader(dataset=RandomDataset(), batch_size=config.batch_size, collate_fn=collate_fn, drop_last=True)

Dataset,Dataloader 完整代码:

config.py

  1. from word_sequence import NumSequence
  2. train_batch_size = 128
  3. num_sequence = NumSequence()
  4. max_len = 9

word_sequence.py   【此文件和上面的稍微有点区别】

  1. class NumSequence(object):
  2. PAD_TAG = 'PAD' # 填充标记
  3. UNK_TAG = 'UNK' # 未知词标记
  4. SOS_TAG = 'SOS' # strat of sequence
  5. EOS_TAG = 'EOS' # end of sequence
  6. PAD = 0
  7. UNK = 1
  8. SOS = 2
  9. EOS = 3
  10. def __init__(self):
  11. self.dict = {
  12. self.PAD_TAG: self.PAD,
  13. self.UNK_TAG: self.UNK,
  14. self.SOS_TAG: self.SOS,
  15. self.EOS_TAG: self.EOS
  16. }
  17. for i in range(10):
  18. self.dict[str(i)] = len(self.dict)
  19. self.inverse_dict = dict(zip(self.dict.values(), self.dict.keys()))
  20. def transform(self, sentence, max_len, add_eos=False):
  21. """
  22. 把sentence 转化为 序列
  23. :param max_len 句子最大长度
  24. :param add_eos 是否添加结束符
  25. add_eos : True时,输出句子长度为max_len + 1
  26. add_eos : False时,输出句子长度为max_len
  27. :return:
  28. """
  29. if len(sentence) > max_len:
  30. sentence = sentence[:max_len]
  31. # 提前计算句子长度,实现add_eos后,句子长度统一
  32. sentence_len = len(sentence)
  33. # sentence[1,3,4,5,UNK,EOS,PAD,PAD....]
  34. if add_eos:
  35. sentence += [self.EOS_TAG]
  36. if sentence_len < max_len:
  37. # 句子长度不够,用PAD填充
  38. sentence += (max_len - sentence_len) * [self.PAD_TAG]
  39. # 对于新出现的词采用特殊标记
  40. result = [self.dict.get(i, self.UNK) for i in sentence]
  41. return result
  42. def invert_transform(self, indices):
  43. """
  44. 序列转化为sentence
  45. :param indices:
  46. :return:
  47. """
  48. return [self.inverse_dict.get(i, self.UNK_TAG) for i in indices]
  49. if __name__ == '__main__':
  50. num_sequence = NumSequence()
  51. print(num_sequence.dict)
  52. print(num_sequence.inverse_dict)

dataset.py

  1. """
  2. 准备数据集,准备dataset,dataloader
  3. """
  4. import config
  5. from torch.utils.data import Dataset, DataLoader
  6. import numpy as np
  7. import torch
  8. class NumDataset(Dataset):
  9. def __init__(self):
  10. # 使用numpy随机创建, 1e8 = 10^8
  11. self.data = np.random.randint(0, 1e8, size=[500000])
  12. def __getitem__(self, index):
  13. input = list(str(self.data[index]))
  14. target = input + ['0']
  15. input_lenth = len(input)
  16. target_lenth = len(target)
  17. # target_length = input_length + 1
  18. return input, target, input_lenth, target_lenth
  19. def __len__(self):
  20. return self.data.shape[0]
  21. def collate_fn(batch):
  22. """
  23. :param batch:[(input,target,input_length,target_length),...,]
  24. :return:
  25. """
  26. batch = sorted(batch, key=lambda x: x[3], reverse=True)
  27. input, target, input_length, target_length = list(zip(*batch))
  28. # 把input 转化为序列
  29. input = torch.LongTensor([config.num_sequence.transform(i, max_len=config.max_len) for i in input])
  30. target = torch.LongTensor(
  31. [config.num_sequence.transform(i, max_len=config.max_len + 1, add_eos=True) for i in target])
  32. input_length = torch.LongTensor(input_length)
  33. target_length = torch.LongTensor(target_length)
  34. return input, target, input_length, target_length
  35. train_dataloader = DataLoader(NumDataset(), batch_size=config.train_batch_size, shuffle=True, collate_fn=collate_fn)
  36. if __name__ == '__main__':
  37. for input, target, input_length, target_length in train_dataloader:
  38. print(input.size())
  39. print(target.size())
  40. print(input)
  41. print(target)
  42. print(input_length)
  43. print(target_length)
  44. break

运行结果:

2.4 准备编码器

编码器(encoder)的目的就是为了对文本进行编码,把编码后的结果交给后续的程序使用,所以在这里可以使用Embedding+GRU的结构来使用,使用最后一个time step的输出(hidden state)作为句子的编码结果

注意点:

  1. Embedding和GRU的参数,这里我们让GRU中batch放在前面

  2. 输出结果的形状

  3. 在LSTM和GRU中,每个time step的输入会进行计算,得到结果,整个过程是一个和句子长度相关的一个循环,手动实现速度较慢

    1. pytorch中实现了nn.utils.rnn.pack_padded_sequence 对padding后的句子进行打包的操作能够更快获得LSTM or GRU的结果

    2. 同时实现了nn.utils.rnn.pad_packed_sequence对打包的内容进行解包的操作

  4. nn.utils.rnn.pack_padded_sequence使用过程中需要对batch中的内容按照句子的长度降序排序

(batch = sorted(batch, key = lambda x : x[3], reverse = True)------->batch:[(input,target,input_length,target_length),...,])

  1. embeded = nn.utils.rnn.pack_padded_sequence
  2. (
  3. embeded,
  4. lengths=input_length, # 真实长度
  5. batch_first=True
  6. )
  7. out,outputs_length = nn.utils.rnn.pad_packed_sequence
  8. (
  9. out,
  10. batch_first=True,
  11. padding_value=num_sequence.PAD # 填充值
  12. )

实现代码如下:

  1. import torch.nn as nn
  2. from word_sequence import num_sequence
  3. import config
  4. class NumEncoder(nn.Module):
  5. def __init__(self):
  6. super(NumEncoder, self).__init__()
  7. self.vocab_size = len(num_sequence)
  8. self.dropout = config.dropout
  9. self.embedding = nn.Embedding(num_embeddings=self.vocab_size, embedding_dim=config.embedding_dim,
  10. padding_idx=num_sequence.PAD)
  11. self.gru = nn.GRU(input_size=config.embedding_dim,
  12. hidden_size=config.hidden_size,
  13. num_layers=1,
  14. batch_first=True)
  15. def forward(self, input, input_length):
  16. """
  17. input:[batch_size,max_len]
  18. input_length:[batch_size]
  19. """
  20. embeded = self.embedding(input) # [batch_size,max_len , embedding_dim]
  21. # 对文本对齐之后的句子进行打包,能够加速在LSTM or GRU中的计算过程
  22. embeded = nn.utils.rnn.pack_padded_sequence(embeded, lengths=input_length, batch_first=True)
  23. # hidden:[1,batch_size,vocab_size]
  24. out, hidden = self.gru(embeded)
  25. # 对前面打包后的结果再进行解包
  26. out, outputs_length = nn.utils.rnn.pad_packed_sequence(out, batch_first=True, padding_value=num_sequence.PAD)
  27. # out [batch_size,seq_len,hidden_size]
  28. return out, hidden

完整实现代码:

config.py

  1. import torch
  2. from word_sequence import NumSequence
  3. train_batch_size = 128
  4. num_sequence = NumSequence()
  5. max_len = 9
  6. embedding_dim = 100
  7. num_layer = 1
  8. hidden_size = 64
  9. model_save_path = './model.pkl'
  10. optimizer_save_path = './optimizer.pkl'
  11. device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')

encode.py

  1. """
  2. 编码器
  3. """
  4. import torch.nn as nn
  5. import config
  6. from torch.nn.utils.rnn import pad_packed_sequence, pack_padded_sequence
  7. class Encoder(nn.Module):
  8. def __init__(self):
  9. super(Encoder, self).__init__()
  10. # torch.nn.Embedding(num_embeddings词典大小即不重复词数,embedding_dim单个词用多长向量表示)
  11. self.embedding = nn.Embedding(
  12. num_embeddings=len(config.num_sequence.dict),
  13. embedding_dim=config.embedding_dim,
  14. padding_idx=config.num_sequence.PAD
  15. )
  16. self.gru = nn.GRU(
  17. input_size=config.embedding_dim,
  18. num_layers=config.num_layer,
  19. hidden_size=config.hidden_size,
  20. bidirectional=False,
  21. batch_first=True
  22. )
  23. def forward(self, input, input_length):
  24. """
  25. :param input: [batch_size, max_len]
  26. :return:
  27. """
  28. embedded = self.embedding(input) # embedded [batch_size, max_len, embedding_dim]
  29. # 加速循环过程
  30. embedded = pack_padded_sequence(embedded, input_length, batch_first=True) # 打包
  31. out, hidden = self.gru(embedded)
  32. out, out_length = pad_packed_sequence(out, batch_first=True, padding_value=config.num_sequence.PAD) # 解包
  33. # hidden即h_n [num_layer*[1/2],batchsize, hidden_size]
  34. # out : [batch_size, seq_len/max_len, hidden_size]
  35. return out, hidden, out_length
  36. if __name__ == '__main__':
  37. from dataset import train_dataloader
  38. encoder = Encoder()
  39. print(encoder)
  40. for input, target, input_length, target_length in train_dataloader:
  41. out, hidden, out_length = encoder(input, input_length)
  42. print(input.size())
  43. print(out.size())
  44. print(hidden.size())
  45. print(out_length)
  46. break

运行结果:

  1. Encoder(
  2. (embedding): Embedding(14, 100, padding_idx=0)
  3. (gru): GRU(100, 64, batch_first=True)
  4. )
  5. torch.Size([128, 9])
  6. torch.Size([128, 8, 64])
  7. torch.Size([1, 128, 64])
  8. tensor([8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
  9. 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
  10. 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
  11. 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
  12. 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 7, 7,
  13. 7, 7, 7, 7, 7, 7, 7, 6])
  14. Process finished with exit code 0

2.5 实现解码器

加码器主要负责实现对编码之后结果的处理,得到预测值,为后续计算损失做准备

此时需要思考:

  1. 使用什么样的损失函数,预测值需要是什么格式的

    • 结合之前的经验,我们可以理解为当前的问题是一个分类的问题,即每次的输出其实对选择一个概率最大的词

    • 真实值的形状是[batch_size,max_len],从而我们知道输出的结果需要是一个[batch_size,max_len,vocab_size]的形状

    • 即预测值的最后一个维度进行计算log_softmax,然后和真实值进行相乘,从而得到损失

  2. 如何把编码结果[1,batch_size,hidden_size]进行操作,得到预测值。解码器也是一个RNN,即也可以使用LSTM or GRU的结构,所以在解码器中:

    • 通过循环,每次计算的一个time step的内容

    • 编码器的结果作为初始的隐层状态,定义一个[batch_size,1]的全为SOS的数据作为最开始的输入,告诉解码器,要开始工作了

    • 通过解码器预测一个输出[batch_size,hidden_size](会进行形状的调整为[batch_size,vocab_size]),把这个输出作为输入再使用解码器进行解码

    • 上述是一个循环,循环次数就是句子的最大长度,那么就可以得到max_len个输出

    • 把所有输出的结果进行concate,得到[batch_size,max_len,vocab_size]

  3. 在RNN的训练过程中,使用前一个预测的结果作为下一个step的输入,可能会导致一步错,步步错的结果,如果提高模型的收敛速度?

    • 可以考虑在训练的过程中,把真实值作为下一步的输入,这样可以避免步步错的局面

    • 同时在使用真实值的过程中,仍然使用预测值作为下一步的输入,两种输入随机使用

    • 上述这种机制我们把它称为Teacher forcing,就像是一个指导老师,在每一步都会对我们的行为进行纠偏,从而达到在多次训练之后能够需要其中的规律

示例代码:

  1. import torch
  2. import torch.nn as nn
  3. import config
  4. import random
  5. import torch.nn.functional as F
  6. from word_sequence import num_sequence
  7. class NumDecoder(nn.Module):
  8. def __init__(self):
  9. super(NumDecoder, self).__init__()
  10. self.max_seq_len = config.max_len
  11. self.vocab_size = len(num_sequence)
  12. self.embedding_dim = config.embedding_dim
  13. self.dropout = config.dropout
  14. self.embedding = nn.Embedding(num_embeddings=self.vocab_size, embedding_dim=self.embedding_dim,
  15. padding_idx=num_sequence.PAD)
  16. self.gru = nn.GRU(input_size=self.embedding_dim,
  17. hidden_size=config.hidden_size,
  18. num_layers=1,
  19. batch_first=True,
  20. dropout=self.dropout)
  21. self.log_softmax = nn.LogSoftmax()
  22. self.fc = nn.Linear(config.hidden_size, self.vocab_size)
  23. def forward(self, encoder_hidden, target, target_length):
  24. # encoder_hidden [batch_size,hidden_size]
  25. # target [batch_size,max_len]
  26. # 初始的全为SOS的输入
  27. decoder_input = torch.LongTensor([[num_sequence.SOS]] * config.batch_size)
  28. # 解码器的输出,用来后保存所有的输出结果
  29. decoder_outputs = torch.zeros(config.batch_size, config.max_len, self.vocab_size)
  30. decoder_hidden = encoder_hidden # [batch_size,hidden_size]
  31. for t in range(config.max_len):
  32. decoder_output_t, decoder_hidden = self.forward_step(decoder_input, decoder_hidden)
  33. # 在不同的time step上进行复制,decoder_output_t [batch_size,vocab_size]
  34. decoder_outputs[:, t, :] = decoder_output_t
  35. # 在训练的过程中,使用 teacher forcing,进行纠偏
  36. use_teacher_forcing = random.random() > 0.5
  37. if use_teacher_forcing:
  38. # 下一次的输入使用真实值
  39. decoder_input = target[:, t].unsqueeze(1) # [batch_size,1]
  40. else:
  41. # 使用预测值,topk中k=1,即获取最后一个维度的最大的一个值
  42. value, index = torch.topk(decoder_output_t, 1) # index [batch_size,1]
  43. decoder_input = index
  44. return decoder_outputs, decoder_hidden
  45. def forward_step(self, decoder_input, decoder_hidden):
  46. """
  47. :param decoder_input:[batch_size,1]
  48. :param decoder_hidden: [1,batch_size,hidden_size]
  49. :return: out:[batch_size,vocab_size],decoder_hidden:[1,batch_size,didden_size]
  50. """
  51. embeded = self.embedding(decoder_input) # embeded: [batch_size,1 , embedding_dim]
  52. out, decoder_hidden = self.gru(embeded, decoder_hidden) # out [1, batch_size, hidden_size]
  53. out = out.squeeze(0) # 去除第0维度的1
  54. # 进行全连接形状变化,同时进行求取log_softmax
  55. out = F.log_softmax(self.fc(out), dim=-1) # out [batch_Size,1, vocab_size]
  56. out = out.squeeze(1)
  57. return out, decoder_hidden

完整代码实现:

decode.py

  1. """
  2. 实现解码器
  3. """
  4. import torch.nn as nn
  5. import config
  6. import torch
  7. import torch.nn.functional as F
  8. class Decoder(nn.Module):
  9. def __init__(self):
  10. super(Decoder, self).__init__()
  11. self.embedding = nn.Embedding(
  12. num_embeddings=len(config.num_sequence),
  13. embedding_dim=config.embedding_dim,
  14. padding_idx=config.num_sequence.PAD
  15. )
  16. self.gru = nn.GRU(
  17. input_size=config.embedding_dim,
  18. hidden_size=config.hidden_size,
  19. num_layers=config.num_layer,
  20. batch_first=True,
  21. bidirectional=False
  22. )
  23. self.fc = nn.Linear(config.hidden_size, len(config.num_sequence))
  24. def forward(self, target, encoder_hidden):
  25. # 1.获取encoder最后一次的输出,作为decoder第一次的隐藏状态
  26. decoder_hidden = encoder_hidden
  27. batch_size = target.size(0)
  28. # 2.准备第一次decoder第一个时间步的输入,[batch_size,1]的SOS作为输入
  29. decoder_input = torch.LongTensor(torch.ones([batch_size, 1], dtype=torch.int64) * config.num_sequence.SOS).to(
  30. config.device)
  31. # 3.在第一个时间步上进行计算,得到第一个时间步的输出,hidden_state
  32. # 4.对前一个时间步的输出进行计算,得到第一个最后的输出的结果
  33. # 5.把前一次的hidden_state作为当前时间步的hidden_state的输入,把前一次的输出,作为当前时间步的输入
  34. # 6.循环4-5
  35. # 保存预测结果
  36. # output:[batch_size, vocab_size]
  37. decoder_outputs = torch.zeros([batch_size, config.max_len + 2, len(config.num_sequence)]).to(config.device)
  38. # config.max_len+2 dataset中获取target时,max_len+1且add_eos=True,因此+2
  39. for t in range(config.max_len + 2):
  40. # 当前时刻的输出和隐藏状态
  41. decoder_output_t, decoder_hidden = self.forward_step(decoder_input, decoder_hidden)
  42. # 保存decoder_output_t到decoder_outputs中
  43. decoder_outputs[:, t, :] = decoder_output_t
  44. # 使用预测值,topk中k = 1,即获取最后一个维度的最大的一个值
  45. value, index = torch.topk(decoder_output_t, k=1)
  46. # 获取下一次的input
  47. decoder_input = index
  48. return decoder_outputs, decoder_hidden
  49. def forward_step(self, decoder_input, decoder_hidden):
  50. """
  51. 计算每个时间步上的结果
  52. :param decoder_input: [batch_size, 1]
  53. :param decoder_hidden: [num_layer * [1/2],batch_size, hidden_size] decoder_hidden也就是encoder最后一次的隐藏状态
  54. :return:
  55. """
  56. decoder_input_embedded = self.embedding(decoder_input) # [batch_size, 1] -->[batch_size, 1, embedding_dim]
  57. # out [batch_size, 1, hidden_size]
  58. # decoder_hidden [num_layer*[1/2],batch_size,hidden_size]
  59. out, decoder_hidden = self.gru(decoder_input_embedded, decoder_hidden)
  60. out = out.squeeze(1) # [batch_size, 1, hidden_size] --> [batch_size, hidden_size]
  61. output = F.log_softmax(self.fc(out), dim=-1) # fc后,out [batch_size, hidden_size]-->[batch_size, vocab_size]
  62. # print('output:',output.size())
  63. return output, decoder_hidden

2.6 完成seq2seq模型

调用之前的encoder和decoder,完成模型的搭建  【合并encode和decode】

  1. import torch
  2. import torch.nn as nn
  3. class Seq2Seq(nn.Module):
  4. def __init__(self, encoder, decoder):
  5. super(Seq2Seq, self).__init__()
  6. self.encoder = encoder
  7. self.decoder = decoder
  8. def forward(self, input, target, input_length, target_length):
  9. # 进行编码
  10. encoder_outputs, encoder_hidden = self.encoder(input, input_length)
  11. # 进行解码
  12. decoder_outputs, decoder_hidden = self.decoder(encoder_hidden, target, target_length)
  13. return decoder_outputs, decoder_hidden

2.7 完成训练逻辑

思路流程和之前相同

示例代码:

  1. import torch
  2. import config
  3. from torch import optim
  4. import torch.nn as nn
  5. from encoder import NumEncoder
  6. from decoder import NumDecoder
  7. from seq2seq import Seq2Seq
  8. from dataset import data_loader as train_dataloader
  9. from word_sequence import num_sequence
  10. encoder = NumEncoder()
  11. decoder = NumDecoder()
  12. model = Seq2Seq(encoder, decoder)
  13. print(model)
  14. # 自定义初始化参数
  15. # for name, param in model.named_parameters():
  16. # if 'bias' in name:
  17. # torch.nn.init.constant_(param, 0.0)
  18. # elif 'weight' in name:
  19. # torch.nn.init.xavier_normal_(param)
  20. # model.load_state_dict(torch.load("model/seq2seq_model.pkl"))
  21. optimizer = optim.Adam(model.parameters())
  22. # optimizer.load_state_dict(torch.load("model/seq2seq_optimizer.pkl"))
  23. criterion = nn.NLLLoss(ignore_index=num_sequence.PAD, reduction="mean")
  24. def get_loss(decoder_outputs, target):
  25. # 很多时候如果tensor进行了转置等操作,直接调用view进行形状的修改是无法成功的
  26. # target = target.contiguous().view(-1) #[batch_size*max_len]
  27. target = target.view(-1)
  28. decoder_outputs = decoder_outputs.view(config.batch_size * config.max_len, -1)
  29. return criterion(decoder_outputs, target)
  30. def train(epoch):
  31. for idx, (input, target, input_length, target_len) in enumerate(train_dataloader):
  32. optimizer.zero_grad()
  33. ##[seq_len,batch_size,vocab_size] [batch_size,seq_len]
  34. decoder_outputs, decoder_hidden = model(input, target, input_length, target_len)
  35. loss = get_loss(decoder_outputs, target)
  36. loss.backward()
  37. optimizer.step()
  38. print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
  39. epoch, idx * len(input), len(train_dataloader.dataset),
  40. 100. * idx / len(train_dataloader), loss.item()))
  41. torch.save(model.state_dict(), "model/seq2seq_model.pkl")
  42. torch.save(optimizer.state_dict(), 'model/seq2seq_optimizer.pkl')
  43. if __name__ == '__main__':
  44. for i in range(10):
  45. train(i)

完整代码:

seq2seq.py

  1. import torch.nn as nn
  2. from encode import Encoder
  3. from decode import Decoder
  4. encoder = Encoder()
  5. decoder = Decoder()
  6. class Seq2Seq(nn.Module):
  7. def __init__(self):
  8. super(Seq2Seq, self).__init__()
  9. self.encoder = Encoder()
  10. self.decoder = Decoder()
  11. def forward(self, input, target, input_length, target_length):
  12. # 进行编码
  13. encoder_outputs, encoder_hidden, out_length = self.encoder(input, input_length)
  14. # 进行解码
  15. # decoder_outputs, decoder_hidden = self.decoder(encoder_hidden, target, target_length)
  16. decoder_outputs, decoder_hidden = self.decoder(target, encoder_hidden)
  17. return decoder_outputs, decoder_hidden

train.py

  1. from dataset import train_dataloader
  2. import os
  3. from encode import Encoder
  4. from decode import Decoder
  5. from seq2seq import Seq2Seq
  6. from torch.optim import Adam
  7. import torch.nn.functional as F
  8. import config
  9. from tqdm import tqdm
  10. import torch
  11. # 训练流程
  12. # 1.实例化model, optimizer, loss
  13. # seq2seq = Seq2Seq(encoder, decoder).to(config.device)
  14. seq2seq = Seq2Seq().to(config.device)
  15. optimizer = Adam(seq2seq.parameters(), lr=0.001)
  16. # 2.遍历dataloader
  17. # 3.调用模型得到output
  18. # 4.计算损失,更新参数
  19. if os.path.exists(config.model_save_path):
  20. seq2seq.load_state_dict(torch.load(config.model_save_path))
  21. optimizer.load_state_dict(torch.load(config.optimizer_save_path))
  22. def train(epoch):
  23. bar = tqdm(train_dataloader, desc='训练', total=len(train_dataloader))
  24. for index, (input, target, input_length, target_length) in enumerate(bar):
  25. input = input.to(config.device)
  26. target = target.to(config.device)
  27. input_length = input_length.to(config.device)
  28. target_length = target_length.to(config.device)
  29. optimizer.zero_grad()
  30. decoder_outputs, _decoder_hidden = seq2seq(input, target, input_length, target_length)
  31. # print(decoder_outputs.size(), target.size())
  32. decoder_outputs = decoder_outputs.view(decoder_outputs.size(0) * decoder_outputs.size(1),
  33. -1) # [batch_size * seq_len, -1]
  34. target = target.view(-1) # [batch_size * seq_len]
  35. loss = F.nll_loss(decoder_outputs, target, ignore_index=config.num_sequence.PAD)
  36. loss.backward()
  37. optimizer.step()
  38. bar.set_description('epoch:{}\tidx{}\tloss{} '.format(epoch, index, loss.item()))
  39. # 5.模型保存和加载
  40. if index % 100 == 0:
  41. torch.save(seq2seq.state_dict(), config.model_save_path)
  42. torch.save(optimizer.state_dict(), config.optimizer_save_path)
  43. if __name__ == '__main__':
  44. for i in range(3):
  45. train(i)

运行结果:

2.8 完成模型评估逻辑

完成评估逻辑,和decoder中的训练过程稍微不同,可以在其中新建evaluation的方法,传入encoder_hidden,得到预测的结果

  1. def evaluation(self, encoder_hidden): # [1, 20, 14]
  2. batch_size = encoder_hidden.size(1) # 评估的时候和训练的batch_size不同,不适用config的配置
  3. decoder_input = torch.LongTensor([[config.num_sequence.SOS] * batch_size])
  4. decoder_outputs = torch.zeros(batch_size, config.max_len, self.vocab_size) # [batch_size,seq_len,vocab_size]
  5. decoder_hidden = encoder_hidden
  6. # 评估,不再使用teacher forcing,完全使用预测值作为下一次的输入
  7. for t in range(config.max_len):
  8. decoder_output_t, decoder_hidden = self.forward_step(decoder_input, decoder_hidden)
  9. decoder_outputs[:, t, :] = decoder_output_t
  10. value, index = torch.topk(decoder_output_t, 1) # index [20,1]
  11. decoder_input = index.transpose(0, 1)
  12. # 获取输出的id
  13. decoder_indices = [] # [[1,2,4],[23,3,2]]
  14. for i in range(config.max_len):
  15. value, index = torch.topk(decoder_outputs[:, i, :], k=1, dim=-1)
  16. decoder_indices.append(index.view(-1).numpy())
  17. # transpose 调整为按句子输出
  18. decoder_indices = np.array(decoder_indices).transpose()
  19. return decoder_indices

之后再seq2seq的model中,添加evaluation的逻辑

示例代码:

  1. import torch
  2. import torch.nn as nn
  3. class Seq2Seq(nn.Module):
  4. def __init__(self,encoder,decoder):
  5. super(Seq2Seq,self).__init__()
  6. self.encoder = encoder
  7. self.decoder = decoder
  8. def forward(self, input,target,input_length,target_length):
  9. encoder_outputs,encoder_hidden = self.encoder(input,input_length)
  10. decoder_outputs,decoder_hidden = self.decoder(encoder_hidden,target,target_length)
  11. return decoder_outputs,decoder_hidden
  12. def evaluation(self,inputs,input_length):
  13. encoder_outputs,encoder_hidden = self.encoder(inputs,input_length)
  14. decoded_sentence = self.decoder.evaluation(encoder_hidden)
  15. return decoded_sentence

更新seq2seq.py

  1. import torch.nn as nn
  2. from encode import Encoder
  3. from decode import Decoder
  4. encoder = Encoder()
  5. decoder = Decoder()
  6. class Seq2Seq(nn.Module):
  7. def __init__(self):
  8. super(Seq2Seq, self).__init__()
  9. self.encoder = encoder
  10. self.decoder = decoder
  11. def forward(self, input, target, input_length, target_length):
  12. # 进行编码
  13. encoder_outputs, encoder_hidden, out_length = self.encoder(input, input_length)
  14. # 进行解码
  15. # decoder_outputs, decoder_hidden = self.decoder(encoder_hidden, target, target_length)
  16. decoder_outputs, decoder_hidden = self.decoder(target, encoder_hidden)
  17. return decoder_outputs, decoder_hidden
  18. def evaluation(self, inputs, input_length):
  19. encoder_outputs, encoder_hidden, out_length = self.encoder(inputs, input_length)
  20. decoded_sentence = self.decoder.evaluation(encoder_hidden)
  21. return decoded_sentence

创建eval.py,完成模型评估的逻辑

  1. import torch
  2. import config
  3. from torch import optim
  4. import torch.nn as nn
  5. from encoder import NumEncoder
  6. from decoder import NumDecoder
  7. from seq2seq import Seq2Seq
  8. from dataset import data_loader as train_dataloader
  9. from word_sequence import num_sequence
  10. import numpy as np
  11. import random
  12. encoder = NumEncoder()
  13. decoder = NumDecoder()
  14. model = Seq2Seq(encoder,decoder)
  15. model.load_state_dict(torch.load("model/seq2seq_model.pkl"))
  16. def evalaute():
  17. data = [str(i) for i in np.random.randint(0, 100000000, [10])]
  18. data = sorted(data,key=lambda x:len(x),reverse=True)
  19. print(data)
  20. _data_length = torch.LongTensor([len(i) for i in data])
  21. _data = torch.LongTensor([num_sequence.transform(i,max_len=config.max_len) for i in data])
  22. output = seq2seq.evaluate(_data,_data_length)
  23. print([num_sequence.inverse_transform(i) for i in output])
  24. if __name__ == '__main__':
  25. evalaute()

目前存在问题???

在model训练一个epoch之后,loss已经很低了,评估输出如下(为True表示预测正确):

  1. 39304187 >>>>> 393041870 True
  2. 41020882 >>>>> 410208820 True
  3. 85784317 >>>>> 857843170 True
  4. 1394232 >>>>> 13942320 True
  5. 44548446 >>>>> 445484460 True
  6. 49457730 >>>>> 494577300 True
  7. 82451872 >>>>> 824518720 True
  8. 64380958 >>>>> 643809580 True
  9. 97501723 >>>>> 975017230 True
  10. 21656800 >>>>> 216568000 True

总结:

  1. seq2seq流程
  2. 1. encoder
  3. a. 对input 进行embedding
  4. b. 对embedding结果进行打包 pack_padded_sequence
  5. c. 传入gru进行计算,得到output和hidden
  6. d. 对output进行解包 pad_packed_sequence
  7. 2. decoder
  8. a. 构造起始符,构造[batch_size, 1]的SOS,作为第一个时间步的输入
  9. b. 对第一个时间步的输入进行embedding,得到embeded
  10. c. 对embedded 进行gru计算,得到output 和 hidden,hidden 作为下一个时间步的hidden,
  11. d. 计算第一个时间步输出的值:第一个时间步的输出进行变形,之后计算log_softmax,得到output,并获取值最大的位置dim = -1,作为第一个时间步的输出
  12. out = out.squeeze(1) # [batch_size, 1, hidden_size] --> [batch_size, hidden_size]
  13. output = F.log_softmax(self.fc(out), dim = -1) # fc后,out [batch_size, hidden_size]-->[batch_size, vocab_size]
  14. e. 保存output
  15. # 保存decoder_output_t到decoder_outputs中
  16. decoder_outputs[:, t, :] = decoder_output_t
  17. f. 第二个时间步,输入有:hidden和第一个时间步输出的具体值(是一个索引),使用teacher_forcing机制,加速训练
  18. # 使用预测值,topk中k = 1,即获取最后一个维度的最大的一个值
  19. value, index = torch.topk(decoder_output_t, k = 1)
  20. # 使用teacher_forcing机制,加速训练
  21. if random.random() > config.teacher.focing:
  22. decoder_input = target[t] # [batch_size, 1]
  23. else:
  24. # 获取下一次的input
  25. decoder_input = index
  26. g. 重复b-f步,重复max_lenth次,即target长度(config.max_len+2 dataset中获取target时,max_len+1且add_eos=True,因此+2
  27. h. 得到decoder_outputs
  28. 3. train
  29. a. output 和 target 计算nll_loss(带权损失),若是三阶,则需要变形
  30. decoder_outputs = decoder_outputs.view(decoder_outputs.size(0) * decoder_outputs.size(1), -1) # [batch_size * seq_len, -1]
  31. target = target.view(-1) # [batch_size * seq_len]
  32. loss = F.nll_loss(decoder_outputs, target, ignore_index = config.num_sequence.PAD)
  33. 4. eval
  34. a. 和decoder大致相同,但是不需要保存output, 只需要batch数据每个时间步的输出
  35. b. 每个时间步的输出放在列表中,其每一列才是输入的最终结果
  36. indices = seq2seq.evaluate(input, input_length)
  37. indices = np.array(indices).transpose()

 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/366668
推荐阅读
相关标签
  

闽ICP备14008679号