当前位置:   article > 正文

pytorch实现聊天机器人学习笔记(1)_怎样用pytorch来控制机器人

怎样用pytorch来控制机器人

数据预处理

本文以医疗智能问答为例子,简单介绍下seq2seq模型下,pytorch如何实现聊天机器人.由于fly平台提供的竞赛数据都很干净,基本上不需要什么预处理.一般情况下预处理操作也就是把杂乱的问答一一对应起来,形成一个数据集.数据集中每一个问答就是一条记录.

生成词典

数据集生成后就需要生成字典.对于中文句子有两种方式生成字典:

  1. 1.逐字生成字典,这样生成的字典大小会小一些.
  2. 2.分词后,根据分词结果生成字典.此处我一般使用jieba分词

读取数据集中每条记录,调用以下代码来生成字典

  • def source2id(self,textline):
  • textline = jieba.lcut(textline)
  • text_list = list()
  • count = len(self.sour2id)
  • for i in range(min(len(textline),128)):
  • if self.sour2id.get(textline[i]):
  • text_list.append(self.sour2id[textline[i]])
  • else:
  • self.sour2id[textline[i]] = count
  • self.id2sour[count] = textline[i]
  • text_list.append(self.sour2id[textline[i]])
  • count += 1
  • text_len = len(text_list)
  • return text_list, text_len

在网上很多文档中会有去掉低频词的操作,但是我个人觉得要视情况而定,不能盲目的去掉低频词.因为有时候关键词反而是低频词语.特别在医疗问答时,很多医疗数据反而是低频的.

为模型准备数据

   由于每一个句子长度不一样,所以需要统一把句子padding成max_length(最长句子的长度).这样方便GPU计算.但是同时padding也会带来一个问题.比如,一个句子长度为:4 ,max_length=10,这样实际上后面6个时刻的value都是padding,最终forward的输出应该是第4个时刻的输出,后面的计算都是无用功.所以会生成一个batch_size的列表来记录每个句子的实际长度.

   原始的输入通常是batch_size个list,表示batch_size个句子.每次输入的shape大小为(batch_size,max_length).但是seq2seq模型中每个模块用的都是GRU(或者LSTM),而GRU中计算t时刻的输出需要依赖t-1时刻生成的状态值.所以无法在同一个GRU模块中计算一个语句的所有时刻的输出.于是我们需要转换下输入的shape.把(batch_size,max_length)转换成(max_length,batch_size).

   上述的两个操作可以通过itertools.zip_longest函数很方便的实现.

  • def zeroPadding(l, fillvalue):
  • return list(itertools.zip_longest(*l, fillvalue=fillvalue))
  • def get_batches(targets, sources, batch_size, source_padding, target_padding):
  • source_list, source_lens = sources
  • target_list, target_lens = targets
  • for batch_i in range(0, len(source_list)//batch_size):
  • start_i = batch_i * batch_size
  • source_batch = source_list[start_i: start_i + batch_size]
  • source_len_batch = source_lens[start_i: start_i + batch_size]
  • target_batch = target_list[start_i: start_i + batch_size]
  • target_len_batch = target_lens[start_i: start_i + batch_size]
  • max_target_len = max(target_lens[start_i: start_i + batch_size])
  • pad_sources_batch = zeroPadding(source_batch, source_padding)
  • pad_targets_batch = zeroPadding(target_batch, target_padding)
  • mask= zeroPadding(target_batch, target_padding)
  • yield pad_targets_batch, pad_sources_batch, target_len_batch,max_target_len, source_len_batch,mask

模型的建立

1.Encoder

Encoder是个RNN,它会遍历输入的每一个Token(词),每个时刻的输入是上一个时刻的隐状态和输入,然后会有一个输出和新的隐状态。这个新的隐状态会作为下一个时刻的输入隐状态。每个时刻都有一个输出,对于seq2seq模型来说,我们通常只保留最后一个时刻的隐状态,认为它编码了整个句子的语义,但是后面我们会用到Attention机制,它还会用到Encoder每个时刻的输出。Encoder处理结束后会把最后一个时刻的隐状态作为Decoder的初始隐状态。
 

输入:

  • input_seq: 一个batch的输入句子,shape是(max_length, batch_size)
  • input_lengths: 一个长度为batch_size的list,表示句子的实际长度。
  • hidden: 初始化隐状态(通常是零),shape是(n_layers x num_directions, batch_size, hidden_size)

输出:

  • outputs: 最后一层GRU的输出向量(双向的向量加在了一起),shape(max_length, batch_size, hidden_size)
  • hidden: 最后一个时刻的隐状态,shape是(n_layers x num_directions, batch_size, hidden_size)
  • class EncoderRNN(nn.Module):
  • def __init__(self, hidden_size, embedding,n_layers=1, dropout=0.1):
  • super(EncoderRNN, self).__init__()
  • self.n_layers = n_layers
  • self.hidden_size = hidden_size
  • self.embedding = embedding
  • # 如果只有一层,那么不进行Dropout,否则使用传入的参数dropout进行GRU的Dropout。
  • self.gru = nn.GRU(hidden_size, hidden_size, n_layers,
  • dropout=(0 if n_layers == 1 else dropout), bidirectional=True)
  • def forward(self, input_seq, input_lengths, hidden=None):
  • # 输入是(max_length, batch),Embedding之后变成(max_length, batch, hidden_size)
  • embedded = self.embedding(input_seq)
  • # Pack padded batch of sequences for RNN module
  • # 因为RNN(GRU)需要知道实际的长度,所以PyTorch提供了一个函数pack_padded_sequence把输入向量和长度pack
  • # 到一个对象PackedSequence里,这样便于使用。
  • packed = torch.nn.utils.rnn.pack_padded_sequence(embedded, input_lengths,enforce_sorted=False)
  • # 通过GRU进行forward计算,需要传入输入和隐变量
  • # 如果传入的输入是一个Tensor (max_length, batch, hidden_size)
  • # 那么输出outputs是(max_length, batch, hidden_size*num_directions)。
  • # 第三维是hidden_size和num_directions的混合,它们实际排列顺序是num_directions在前面,因此我们可以
  • # 使用outputs.view(seq_len, batch, num_directions, hidden_size)得到4维的向量。
  • # 其中第三维是方向,第四位是隐状态。
  • # 而如果输入是PackedSequence对象,那么输出outputs也是一个PackedSequence对象,我们需要用
  • # 函数pad_packed_sequence把它变成一个shape为(max_length, batch, hidden*num_directions)的向量以及
  • # 一个list,表示输出的长度,当然这个list和输入的input_lengths完全一样,因此通常我们不需要它。
  • outputs, hidden = self.gru(packed, hidden)
  • # 参考前面的注释,我们得到outputs为(max_length, batch, hidden*num_directions)
  • outputs, _ = torch.nn.utils.rnn.pad_packed_sequence(outputs)
  • # 我们需要把输出的num_directions双向的向量加起来
  • # 因为outputs的第三维是先放前向的hidden_size个结果,然后再放后向的hidden_size个结果
  • # 所以outputs[:, :, :self.hidden_size]得到前向的结果
  • # outputs[:, :, self.hidden_size:]是后向的结果
  • # 注意,如果bidirectional是False,则outputs第三维的大小就是hidden_size,
  • # 这时outputs[:, : ,self.hidden_size:]是不存在的,因此也不会加上去。
  • # 对Python slicing不熟的读者可以看看下面的例子:
  • # >>> a=[1,2,3]
  • # >>> a[:3]
  • # [1, 2, 3]
  • # >>> a[3:]
  • # []
  • # >>> a[:3]+a[3:]
  • # [1, 2, 3]
  • # 这样就不用写下面的代码了:
  • # if bidirectional:
  • # outputs = outputs[:, :, :self.hidden_size] + outputs[:, : ,self.hidden_size:]
  • outputs = outputs[:, :, :self.hidden_size] + outputs[:, : ,self.hidden_size:]
  • # 返回最终的输出和最后时刻的隐状态。
  • return outputs, hidden

2.Decoder

Decoder也是一个RNN,它每个时刻输出一个词。每个时刻的输入是上一个时刻的隐状态和上一个时刻的输出。一开始的隐状态是Encoder最后时刻的隐状态,输入是特殊的。然后使用RNN计算新的隐状态和输出第一个词,接着用新的隐状态和第一个词计算第二个词,…,直到遇到,结束输出。普通的RNN Decoder的问题是它只依赖与Encoder最后一个时刻的隐状态,虽然理论上这个隐状态(context向量)可以编码输入句子的语义,但是实际会比较困难。因此当输入句子很长的时候,效果会很长。

输入:

input_step: shape是(1, batch_size)

last_hidden: 上一个时刻的隐状态, shape是(n_layers x num_directions, batch_size, hidden_size)

encoder_outputs: encoder的输出, shape是(max_length, batch_size, hidden_size)

输出:

output: 当前时刻输出每个词的概率,shape是(batch_size, voc.num_words)

hidden: 新的隐状态,shape是(n_layers x num_directions, batch_size, hidden_size)

class AttnDecoderRNN(nn.Module):

  • def __init__(self, attn_model, embedding, hidden_size, output_size, n_layers=1, dropout=0.1):
  • super(AttnDecoderRNN, self).__init__()
  • self.attn_model = attn_model
  • self.hidden_size = hidden_size
  • self.output_size = output_size
  • self.n_layers = n_layers
  • self.dropout = dropout
  • # 定义Decoder的layers
  • self.embedding = embedding
  • self.embedding_dropout = nn.Dropout(dropout)
  • self.gru = nn.GRU(hidden_size, hidden_size, n_layers, dropout=(0 if n_layers == 1 else dropout))
  • self.concat = nn.Linear(hidden_size * 2, hidden_size)
  • self.out = nn.Linear(hidden_size, output_size)
  • self.attn = Attn(attn_model, hidden_size)
  • def forward(self, input_step, last_hidden, encoder_outputs):
  • # 注意:decoder每一步只能处理一个时刻的数据,因为t时刻计算完了才能计算t+1时刻。
  • # input_step的shape是(1, 64),64是batch,1是当前输入的词ID(来自上一个时刻的输出)
  • # 通过embedding层变成(1, 64, 500),然后进行dropout,shape不变。
  • embedded = self.embedding(input_step)
  • embedded = self.embedding_dropout(embedded)
  • # 把embedded传入GRU进行forward计算
  • # 得到rnn_output的shape是(1, 64, 500)
  • # hidden是(2, 64, 500),因为是双向的GRU,所以第一维是2。
  • rnn_output, hidden = self.gru(embedded, last_hidden)
  • # 计算注意力权重, 根据前面的分析,attn_weights的shape是(64, 1, 10)
  • attn_weights = self.attn(rnn_output, encoder_outputs)
  • # encoder_outputs是(10, 64, 500)
  • # encoder_outputs.transpose(0, 1)后的shape是(64, 10, 500)
  • # attn_weights.bmm后是(64, 1, 500)
  • # bmm是批量的矩阵乘法,第一维是batch,我们可以把attn_weights看成64个(1,10)的矩阵
  • # 把encoder_outputs.transpose(0, 1)看成64个(10, 500)的矩阵
  • # 那么bmm就是64个(1, 10)矩阵 x (10, 500)矩阵,最终得到(64, 1, 500)
  • context = attn_weights.bmm(encoder_outputs.transpose(0, 1))
  • # 把context向量和GRU的输出拼接起来
  • # rnn_output从(1, 64, 500)变成(64, 500)
  • rnn_output = rnn_output.squeeze(0)
  • # context从(64, 1, 500)变成(64, 500)
  • context = context.squeeze(1)
  • # 拼接得到(64, 1000)
  • concat_input = torch.cat((rnn_output, context), 1)
  • # self.concat是一个矩阵(1000, 500),
  • # self.concat(concat_input)的输出是(64, 500)
  • # 然后用tanh把输出返回变成(-1,1),concat_output的shape是(64, 500)
  • concat_output = torch.tanh(self.concat(concat_input))
  • # out是(500, 词典大小=7826)
  • output = self.out(concat_output)
  • # 用softmax变成概率,表示当前时刻输出每个词的概率。
  • output = F.softmax(output, dim=1)
  • # 返回 output和新的隐状态
  • return output, hidden

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可,转载请附上原文出处链接和本声明。 
本文链接地址:https://www.flyai.com/article/573

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

闽ICP备14008679号