赞
踩
本文以医疗智能问答为例子,简单介绍下seq2seq模型下,pytorch如何实现聊天机器人.由于fly平台提供的竞赛数据都很干净,基本上不需要什么预处理.一般情况下预处理操作也就是把杂乱的问答一一对应起来,形成一个数据集.数据集中每一个问答就是一条记录.
数据集生成后就需要生成字典.对于中文句子有两种方式生成字典:
1.逐字生成字典,这样生成的字典大小会小一些.
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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。