1. seq2seq中使用teacher forcing
【用在训练阶段的Decoder中】
在前面的seq2seq的案例中,我们介绍了teacher frocing
是什么,当时我们的输入和输出很相似,所以当时我们的teacher forcing
是在每个time step中实现的,那么现在我们的输入和输出不同的情况下,该如何使用呢?
我们可以在decoder的每个batch遍历time step的外层使用teacher forcing
代码如下:
use_teacher_forcing = random.random() > 0.5
if use_teacher_forcing: #使用teacher forcing
for t in range(config.max_len):
decoder_output_t, decoder_hidden, decoder_attn_t = self.forward_step(decoder_input, decoder_hidden,
encoder_outputs)
decoder_outputs[:, t, :] = decoder_output_t
#使用正确的输出作为下一步的输入
decoder_input = target[:, t].unsqueeze(1) # [batch_size,1]
else:#不适用teacher forcing,使用预测的输出作为下一步的输入
for t in range(config.max_len):
decoder_output_t ,decoder_hidden,decoder_attn_t = self.forward_step(decoder_input,decoder_hidden,encoder_outputs)
decoder_outputs[:,t,:] = decoder_output_t
value, index = torch.topk(decoder_output_t, 1) # index [batch_size,1]
decoder_input = index
解码器的完整代码:
import torch
import torch.nn as nn
import config
import random
import torch.nn.functional as F
from word_sequence import word_sequence
class Decoder(nn.Module):
def __init__(self):
super(Decoder,self).__init__()
self.max_seq_len = config.max_len
self.vocab_size = len(word_sequence)
self.embedding_dim = config.embedding_dim
self.dropout = config.dropout
self.embedding = nn.Embedding(num_embeddings=self.vocab_size,embedding_dim=self.embedding_dim,padding_idx=word_sequence.PAD)
self.gru = nn.GRU(input_size=self.embedding_dim,
hidden_size=config.hidden_size,
num_layers=1,
batch_first=True,
dropout=self.dropout)
self.log_softmax = nn.LogSoftmax()
self.fc = nn.Linear(config.hidden_size,self.vocab_size)
def forward(self, encoder_hidden,target,target_length):
# encoder_hidden [batch_size,hidden_size*2]
# target [batch_size,seq-len]
decoder_input = torch.LongTensor([[word_sequence.SOS]]*config.batch_size).to(config.device) # 初始化解码器的input
decoder_hidden = encoder_hidden # 初始化解码器的hidden_state, 形状为:[batch_size,hidden_size*2]【*2是因为编码器使用了bidirectional,所以编码器的输出维度为hidden_size*2】
decoder_outputs = torch.zeros(config.batch_size,config.max_len,self.vocab_size).to(config.device) # 初始化解码器的输出,形状为: [batch_size,seq_len,14]
if random.random() > 0.5:
for t in range(config.max_len):
decoder_output_t , decoder_hidden = self.forward_step(decoder_input,decoder_hidden)
decoder_outputs[:,t,:] = decoder_output_t
value, index = torch.topk(decoder_output_t, 1) # 获取当前时间步的预测值 index [batch_size,1]
decoder_input = index # 使用当前时间步的预测值作为下一个时间步的输入
else:
for t in range(config.chatbot_target_max_len):
decoder_output_t , decoder_hidden = self.forward_step(decoder_input,decoder_hidden)
decoder_outputs[:,t,:] = decoder_output_t
decoder_input = target[:,t].unsqueeze(-1) #把真实值作为下一步的输入
return decoder_outputs,decoder_hidden
def forward_step(self,decoder_input,decoder_hidden):
"""
:param decoder_input:[batch_size,1]
:param decoder_hidden: [1,batch_size,hidden_size*2]
:return: out:[batch_size,vocab_size],decoder_hidden:[1,batch_size,hidden_size*2]
"""
embeded = self.embedding(decoder_input) #embeded: [batch_size,1 , embedding_dim]
out,decoder_hidden = self.gru(embeded,decoder_hidden) #out [1, batch_size, hidden_size*2], decoder_hidden:[1,batch_size,hidden_size*2]
out = out.squeeze(0)
out = F.log_softmax(self.fc(out),dim=-1)#[batch_Size, vocab_size]
out = out.squeeze(1)
# print("out size:",out.size(),decoder_hidden.size())
return out,decoder_hidden
2. 使用梯度裁剪
前面,我们给大家介绍了梯度消失(梯度过小,在多层计算后导致其值太小而无法计算)
和梯度爆炸(梯度过大,导致其值在多层的计算后太大而无法计算)
。
在常见的深度神经网络中,特别是RNN中,我们经常会使用梯度裁剪
的手段,来抑制过大的梯度,能够有效防止梯度爆炸。
梯度裁剪的实现非常简单,仅仅只需要设置一个阈值,把梯度大于该阈值时设置为该阈值。
实现代码:
loss.backward()
#进行梯度裁剪
nn.utils.clip_grad_norm_(model.parameters(),[5,10,15])
optimizer.step()
3、使用Attention机制【用在Decoder中】
4、BeamSearch算法预测【替代贪心算法预测】
4.1 Beam Search的介绍
在进行模型评估的过程中,每次我们选择概率最大的token id作为输出,那么整个输出的句子的概率就是最大的么?
Beam search
的又被称作束集搜索
,是一种seq2seq中用来优化输出结果的算法(不在训练过程中使用,而是在评估或预测时使用)。
例如:传统的获取解码器输出的过程中,每次只选择概率最大的那个结果,作为当前时间步的输出,等到输出结束,我们会发现,整个句子可能并不通顺。虽然在每一个时间步上的输出确实是概率最大的,但是整体的概率确不一定最大的,我们经常把它叫做greedy search[贪心算法]
为了解决上述的问题,可以考虑计算全部的输出的概率乘积,选择最大概率的路径,这样可以达到全局最优解
。但是这样的话,意味着如果句子很长,候选词很多,那么需要保存的数据就会非常大,需要计算的数据量就很大
那么Beam Search 就是介于上述两种方法的一个这种的方法,假设Beam width=2,表示每次保存的最大的概率的个数,这里每次保存两个,在下一个时间步骤一样,也是保留两个,这样就可以达到约束搜索空间大小的目的,从而提高算法的效率。Beam Search也不是全局最优
。【维特比算法是全局最优】
- beam width 是一个超参数。
- beam width =1 时,就是贪心算法,
- beam width=候选词的时候,就是计算全部的概率。
比如在下图中:
使用一个树状图来表示每个time step的可能输出,其中的数字表示是条件概率
黄色的箭头表示的是一种greedy search,概率并不是最大的
如果把beam width设置为2,那么后续可以找到绿色路径的结果,这个结果是最大的
下图是要给beam width=3的例子
- 首先输入
start token <s>
,然后得到四个输出(这里假设一个就四个输出:X,Y,Z,</s>
),选择概率最大三个,X,Y,W - 然后分别把X,Y,W放到下一个time step中作为输入,分别得到三组不同的输出(一共12个输出序列:XX, XY, XZ, X</s>,YX, YY, YZ, Y</s>,WX, WY, WZ, W</s>),这12个输出中找到三个输出概率最大的三个(如下图:XX,XY,WY )保存到Beam中。
- 然后分别把XX,XY,WY放到下一个time step中作为输入,分别得到三组不同的输出(一共12个输出序列:XXX, XXY, XXZ, XX</s>,XYX, XYY, XYZ, XY</s>,WYX, WYY, WYZ, WY</s>),这12个输出中找到三个输出概率最大的三个(如下图:XXX,XYX,WYX )保存到Beam中。
- 然后分别把XXX,XYX,WYX放到下一个time step中作为输入,分别得到三组不同的输出(一共12个输出序列:XXXX, XXXY, XXXZ, XXX</s>,XYXX, XYXY, XYXZ, XYX</s>,WYXX, WYXY, WYXZ, WYX</s>),这12个输出中找到三个输出概率最大的三个(如下图:XYXW,XYXX,WYX</s> )保存到Beam中。
- 然后分别把XYXW,XYXX,WYX</s>放到下一个time step中作为输入,分别得到三组不同的输出(一共12个输出序列:XYXWX, XYXWY, XYXWZ, XYXW</s>,XYXXX, XYXXY, XYXXZ, XYXX</s>,WYX</s>X, WYX</s>Y, WYX</s>Z, WYX</s></s>),这12个输出中找到三个输出概率最大的三个(如下图:XYXW</s>,XYXWY,XYXX</s>)保存到Beam中。
- 继续重复上述步骤,直到获得结束符时的序列概率最大或者是达到句子的最大长度max_len,结束循环。此时选择概率乘积最大的一个路径。
- 拼接整个路径上概率最大的所有结果,比如这里可能是
<s>,X,Y,X,W,</s>
4.2 Beam Search解释
对于MLE算法训练的模型,beam search只在预测的时候需要。训练的时候因为知道正确答案,并不需要再进行这个搜索。
预测的时候,假设词表大小为3,内容为a,b,c。beam size是2,decoder解码的时候:
- 生成第1个词的时候,选择概率最大的2个词,假设为a,c,那么当前的2个序列就是a、c。
- 生成第2个词的时候,我们将当前序列a和c,分别与词表中的所有词进行组合,得到新的6个序列aa ab ac ca cb cc,计算每个序列的得分并选择得分最高2个序列,作为新的当前序列,假如为aa、cb。
- 后面会不断重复这个过程,直到遇到结束符或者达到最大长度为止。最终输出得分最高的2个序列。
4.3 Beam serach的实现
在上述描述的思路中,我们需要注意以下几个内容:
- 数据该如何保存,每一次的输出的最大的beam width个结果,和之后之前的结果该如何保存
- 保存了之后的概率应该如何比较大小,保留下概率最大的三个
- 不能够仅仅只保存当前概率最大的信息,还需要有当前概率最大的三个中,前面的路径的输出结果
4.3.1 数据结构-堆-的认识
对于上面所说的,保留有限个数据,同时需要根据大小来保留,可以使用一种带有优先级的数据结构来实现,这里我们可以使用堆
这种数据结构
堆
是一种优先级的队列,但是他其实并不是队列
队列
都是先进先出或者是先进后出,堆
只根据优先级的高低来取出数据。栈
是一种先进后出的数据结构,有入栈和出栈的操作
在python自带的模块中,有一个叫做heapq
的模块,提供了堆所有的方法。通过下面的代码我们来了解下heapq的使用方法
my_heap = [] #使用列表保存数据
#往列表中插入数据,优先级使用插入的内容来表示,就是一个比较大小的操作,越大优先级越高
heapq.heappush(my_heap,[29,True,"xiaohong"])
heapq.heappush(my_heap,[28,False,"xiaowang"])
heapq.heappush(my_heap,[29,False,"xiaogang"])
for i in range(3):
ret= heapq.heappop(my_heap) #pop操作,优先级最小的数据
print(ret)
#输出如下:
[28, False, 'xiaowang']
[29, False, 'xiaogang']
[29, True, 'xiaohong']
可以发现,输出的顺序并不是数据插入的顺序,而是根据其优先级,从小往大pop(False<True)。
4.3.2 使用堆来实现beam search
为了实现数据的的保存,我们可以把beam search中的数据保存在堆中,同时在往这个堆中添加数据的同时,判断数据的个数,仅仅保存beam width个数据
class Beam:
def __init__(self):
self.heap = list() #保存数据的位置
self.beam_width = config.beam_width #保存数据的总数
def add(self,probility,complete,seq,decoder_input,decoder_hidden):
"""
添加数据,同时判断总的数据个数,多则删除
:param probility: 概率乘积
:param complete: 最后一个是否为EOS
:param seq: list,所有token的列表
:param decoder_input: 下一次进行解码的输入,通过前一次获得
:param decoder_hidden: 下一次进行解码的hidden,通过前一次获得
:return:
"""
heapq.heappush(self.heap,[probility,complete,seq,decoder_input,decoder_hidden])
#判断数据的个数,如果大,则弹出。保证数据总个数小于等于3
if len(self.heap)>self.beam_width:
heapq.heappop(self.heap)
def __iter__(self):#让该beam能够被迭代
return iter(self.heap)
实现方法,完成模型eval过程中的beam search搜索
思路:
- 构造
<SOS>
开始符号等第一次输入的信息,保存在堆中 - 取出堆中的数据,进行forward_step的操作,获得当前时间步的output,hidden
- 从output中选择topk(k=beam width)个输出,作为下一次的input
- 把下一个时间步骤需要的输入等数据保存在一个新的堆中
- 获取新的堆中的优先级最高(概率最大)的数据,判断数据是否是EOS结尾或者是否达到最大长度,如果是,停止迭代
- 如果不是,则重新遍历新的堆中的数据
代码如下
# decoder中的新方法
def evaluatoin_beamsearch_heapq(self,encoder_outputs,encoder_hidden):
"""使用 堆 来完成beam search,对是一种优先级的队列,按照优先级顺序存取数据"""
batch_size = encoder_hidden.size(1)
#1. 构造第一次需要的输入数据,保存在堆中
decoder_input = torch.LongTensor([[word_sequence.SOS] * batch_size]).to(config.device)
decoder_hidden = encoder_hidden #需要输入的hidden
prev_beam = Beam()
prev_beam.add(1,False,[decoder_input],decoder_input,decoder_hidden)
while True:
cur_beam = Beam()
#2. 取出堆中的数据,进行forward_step的操作,获得当前时间步的output,hidden
#这里使用下划线进行区分
for _probility,_complete,_seq,_decoder_input,_decoder_hidden in prev_beam:
#判断前一次的_complete是否为True,如果是,则不需要forward
#有可能为True,但是概率并不是最大
if _complete == True:
cur_beam.add(_probility,_complete,_seq,_decoder_input,_decoder_hidden)
else:
decoder_output_t, decoder_hidden,_ = self.forward_step(_decoder_input, _decoder_hidden,encoder_outputs)
value, index = torch.topk(decoder_output_t, config.beam_width) # [batch_size=1,beam_widht=3]
#3. 从output中选择topk(k=beam width)个输出,作为下一次的input
for m, n in zip(value[0], index[0]):
decoder_input = torch.LongTensor([[n]]).to(config.device)
seq = _seq + [n]
probility = _probility * m
if n.item() == word_sequence.EOS:
complete = True
else:
complete = False
#4. 把下一个实践步骤需要的输入等数据保存在一个新的堆中
cur_beam.add(probility,complete,seq,decoder_input,decoder_hidden)
#5. 获取新的堆中的优先级最高(概率最大)的数据,判断数据是否是EOS结尾或者是否达到最大长度,如果是,停止迭代
best_prob,best_complete,best_seq,_,_ = max(cur_beam)
if best_complete == True or len(best_seq)-1 == config.max_len: #减去sos
return self._prepar_seq(best_seq)
else:
#6. 则重新遍历新的堆中的数据
prev_beam = cur_beam
def _prepar_seq(self,seq):#对结果进行基础的处理,共后续转化为文字使用
if seq[0].item() == word_sequence.SOS:
seq= seq[1:]
if seq[-1].item() == word_sequence.EOS:
seq = seq[:-1]
seq = [i.item() for i in seq]
return seq
4.3.3 修改seq2seq模型
在seq2seq中使用evaluatoin_beamsearch_heapq查看效果,会发现使用beam search的效果比单独使用attention的效果更好
使用小黄鸡语料(50万个问答),单个字作为token,5个epoch之后的训练结果,左边为问,右边是回答
你在干什么 >>>>> 你想干啥?
你妹 >>>>> 不是我
你叫什么名字 >>>>> 你猜
你个垃圾 >>>>> 你才是,你
你是傻逼 >>>>> 是你是傻
笨蛋啊 >>>>> 我不是,你
5、其他优化模型方法
- 参数的初始化
- 优化现有的数据,语料
- 数据清洗
- 标点、表情、外文的处理
- 把时间、人名、地点等名词替换成对应的各自符号
- 从不懂角度,不同复杂程度去准备语料
- 角度:天气,吃饭,性别…
- 复杂度:简单、一般、复杂
- 数据清洗
- 工程的角度优化
- 使用模板,对常见的问题进行匹配,返回预设的答案
- 使用分类模型,进行对问题的分类,返回预设的答案
- 使用搜索模型,从现有的语料库中,返回相似问题对应的答案。
- 根据特定的问题,使用分类模型进行训练,然后再训练单独的回个该为题的为模型
- 比如询问名字,可以使用fasttext先进行意图识别,命中
询问名字
分类后,直接返回名字 - 或者是手动构造和名字相关的很多问题,来进行训练,从而能够更加个性化的回答出结果
- 比如询问名字,可以使用fasttext先进行意图识别,命中
- 直接对现有的语料进行修改和清洗,把语料中更多的答案进行替换,比如咨询名字的,咨询天气的等,这样能够更大程度上的回答出更加规范的答案
- 使用搜索模型,不再使用这种生成模型