当前位置:   article > 正文

人工智能NLP项目_Seq2Seq模型和闲聊机器人(6)_基于sq2sqe和rnn聊天机器人

基于sq2sqe和rnn聊天机器人

闲聊机器人的介绍

目标

  1. 了解闲聊机器人是什么

介绍

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

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

常见的闲聊模型都是一种seq2seq的结构,在后面的课程中我们会学习并使用seq2seq来实现我们的闲聊机器人

Seq2Seq模型的原理

目标

  1. 知道seq2seq的常见应用场景

  2. 能够说出常见的seq2seq的结构

  3. 能够使用代码完成基础的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)
  20. #得到数字和字符串对应的字典
  21. self.index2word = dict(zip(self.dict.values(),self.dict.keys()))
  22. def __len__(self):
  23. return len(self.dict)
  24. def transform(self,sequence,max_len=None,add_eos=False):
  25. """
  26. sequence:句子
  27. max_len :句子的最大长度
  28. add_eos:是否添加结束符
  29. """
  30. sequence_list = list(str(sequence))
  31. seq_len = len(sequence_list)+1 if add_eos else len(sequence_list)
  32. if add_eos and max_len is not None:
  33. assert max_len>= seq_len, "max_len 需要大于seq+eos的长度"
  34. _sequence_index = [self.dict.get(i,self.UNK) for i in sequence_list]
  35. if add_eos:
  36. _sequence_index += [self.EOS]
  37. if max_len is not None:
  38. sequence_index = [self.PAD]*max_len
  39. sequence_index[:seq_len] = _sequence_index
  40. return sequence_index
  41. else:
  42. return _sequence_index
  43. def inverse_transform(self,sequence_index):
  44. result = []
  45. for i in sequence_index:
  46. if i==self.EOS:
  47. break
  48. result.append(self.index2word.get(int(i),self.UNK_TAG))
  49. return result
  50. # 实例化,供后续调用
  51. num_sequence = NumSequence()
  52. if __name__ == '__main__':
  53. num_sequence = NumSequence()
  54. print(num_sequence.dict)
  55. print(num_sequence.index2word)
  56. print(num_sequence.transform("1231230",add_eos=True))

2.3 准备数据集

2.3.1 准备Dataset

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

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

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中的内容按照句子的长度降序排序

实现代码如下:

  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,padding_idx=num_sequence.PAD)
  10. self.gru = nn.GRU(input_size=config.embedding_dim,
  11. hidden_size=config.hidden_size,
  12. num_layers=1,
  13. batch_first=True)
  14. def forward(self, input,input_length):
  15. """
  16. input:[batch_size,max_len]
  17. input_length:[batch_size]
  18. """
  19. embeded = self.embedding(input) #[batch_size,max_len , embedding_dim]
  20. #对文本对齐之后的句子进行打包,能够加速在LSTM or GRU中的计算过程
  21. embeded = nn.utils.rnn.pack_padded_sequence(embeded,lengths=input_length,batch_first=True)
  22. #hidden:[1,batch_size,vocab_size]
  23. out,hidden = self.gru(embeded)
  24. #对前面打包后的结果再进行解包
  25. out,outputs_length = nn.utils.rnn.pad_packed_sequence(out,batch_first=True,padding_value=num_sequence.PAD)
  26. # out [batch_size,seq_len,hidden_size]
  27. return out,hidden

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,padding_idx=num_sequence.PAD)
  15. self.gru = nn.GRU(input_size=self.embedding_dim,
  16. hidden_size=config.hidden_size,
  17. num_layers=1,
  18. batch_first=True,
  19. dropout=self.dropout)
  20. self.log_softmax = nn.LogSoftmax()
  21. self.fc = nn.Linear(config.hidden_size,self.vocab_size)
  22. def forward(self, encoder_hidden,target,target_length):
  23. # encoder_hidden [batch_size,hidden_size]
  24. # target [batch_size,max_len]
  25. #初始的全为SOS的输入
  26. decoder_input = torch.LongTensor([[num_sequence.SOS]]*config.batch_size)
  27. #解码器的输出,用来后保存所有的输出结果
  28. decoder_outputs = torch.zeros(config.batch_size,config.max_len,self.vocab_size)
  29. decoder_hidden = encoder_hidden #[batch_size,hidden_size]
  30. for t in range(config.max_len):
  31. decoder_output_t , decoder_hidden = self.forward_step(decoder_input,decoder_hidden)
  32. #在不同的time step上进行复制,decoder_output_t [batch_size,vocab_size]
  33. decoder_outputs[:,t,:] = decoder_output_t
  34. #在训练的过程中,使用 teacher f
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小小林熬夜学编程/article/detail/337074
推荐阅读
相关标签
  

闽ICP备14008679号