NLP自然语言处理学习(六)Beam Search

文章目录

前言
一、Beam Search
2 Beam Search的实现
2.1数据结构-堆
2.2 使用堆来实现Beam Search
2.3 模型的优化方法
2.3.1 使用梯度裁剪
其他优化方法


前言

提示:这里可以添加本文要记录的大概内容:
例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。

提示:以下是本篇文章正文内容,下面案例可供参考

1 Beam Search

在进行模型评估的过程中,每次我们选择概率最大的token id作为输出,那么整个输出的句子的概率就是最大的么?

Beam search的又被称作束集搜索,是一种seq2seq中用来优化输出结果的算法(不在训练过程中使用)。

例如:传统的获取解码器输出的过程中,每次只选择概率最大的那个结果,作为当前时间步的输出,等到输出结束,我们会发现,整个句子可能并不通顺。虽然在每一个时间步上的输出确实是概率最大的,但是整体的概率确不一定最大的,我们经常把它叫做greedy search[贪心算法]

为了解决上述的问题,可以考虑计算全部的输出的概率乘积,选择最大的哪一个,但是这样的话,意味着如果句子很长,候选词很多,那么需要保存的数据就会非常大,需要计算的数据量就很大

那么Beam Search 就是介于上述两种方法的一个这种的方法,假设Beam width=2,表示每次保存的最大的概率的个数,这里每次保存两个,在下一个时间步骤一样,也是保留两个,这样就可以达到约束搜索空间大小的目的,从而提高算法的效率。

beam width =1 时,就是贪心算法,beam width=候选词的时候,就是计算全部的概率。beam width 是一个超参数(集束搜索的宽度)。

比如在下图中:

使用一个树状图来表示每个time step的可能输出,其中的数字表示是条件概率

黄色的箭头表示的是一种greedy search,概率并不是最大的

如果把beam width设置为2,那么后续可以找到绿色路径的结果,这个结果是最大的

下图是要给beam width=3的例子

首先输入start token <s>,然后得到四个输出(这里假设一个就四个输出:x,y,z,</s>),选择概率最大三个,x,y,w
然后分别把x,y,z放到下一个time step中作为输入,分别得到三个不同的输出,找到三个输出中概率最大的三个,x,y,y
继续重复上述步骤,直到获得结束符(概率最大)或者是达到句子的最大长度,那么此时选择概率乘积最大的一个。
拼接整个路径上概率最大的所有结果,比如这里可能是<s>,y,y,x,w,</s>


2 Beam Search的实现

在上述描述的思路中,我们需要注意以下几个内容:

数据该如何保存,每一次的输出的最大的beam width个结果,和之后之前的结果该如何保存
保存了之后的概率应该如何比较大小,保留下概率最大的三个
不能够仅仅只保存当前概率最大的信息,还需要有当前概率最大的三个中,前面的路径的输出结果


2.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)。

2.2 使用堆来实现Beam Search

为了实现数据的的保存,我们可以把beam search中的数据保存在堆中,同时在往这个堆中添加数据的同时,判断数据的个数,仅仅保存beam width个数据。

简单的Beam代码实现如下:

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)



思路:

构造<SOS>开始符号等第一次输入的信息,保存在堆中
取出堆中的数据,进行forward_step的操作,获得当前时间步的output,hidden
从output中选择topk(k=beam width)个输出,作为下一次的input
把下一个时间步骤需要的输入等数据保存在一个新的堆中
获取新的堆中的优先级最高(概率最大)的数据,判断数据是否是EOS结尾或者是否达到最大长度,如果是,停止迭代
如果不是,则重新遍历新的堆中的数据
具体实现代码如下:

    def eval_with_Beam_Search(self,encoder_outputs,encoder_hidden):
        batch_size=encoder_hidden.size(1)
        decoder_input=torch.LongTensor([input_ws.SOS]*batch_size)
        decoder_hidden=encoder_hidden
        prev_beam=Beam()
        prev_beam.add(1,False,[decoder_input],decoder_input,decoder_hidden)
        while True:
            cur_beam=Beam()
            for _probility,_complete,_seq,_decoder_input,_decoder_hidden in prev_beam:
                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,k=cur_beam.beam_width)
                    for m,n in zip(value[0],index[0]):
                        decoder_input=torch.LongTensor([[n]])
                        seq=_seq+[n]
                        probility=_probility*m
                        if n.item()==input_ws.EOS:
                            complete=True
                        else:
                            complete=False
                        cur_beam.add(probility,complete,seq,decoder_input,decoder_hidden)
            best_prob,best_complete,best_seq,_,_=max(cur_beam)
            if best_complete==True or len(best_seq)-1==input_ws.max_len:
                return self._prepar_seq(best_seq)
            else:
                prev_beam=cur_beam
    def _prepar_seq(self,seq):
        if seq[0].item()==input_ws.SOS:
            seq=seq[1:]
        if seq[-1].item()==input_ws.EOS:
            seq=seq[:-1]
        seq=[i.item() for i in seq]
        return seq


2.3 模型的优化方法

2.3.1 使用梯度裁剪

前面,我们给大家介绍了梯度消失(梯度过小,在多层计算后导致其值太小而无法计算)和梯度爆炸(梯度过大,导致其值在多层的计算后太大而无法计算)。

在常见的深度神经网络中,特别是RNN中,我们经常会使用梯度裁剪的手段,来抑制过大的梯度,能够有效防止梯度爆炸。

梯度裁剪的实现非常简单,仅仅只需要设置一个阈值,把梯度大于该阈值时设置为该阈值。

实现代码:

loss.backward()
#进行梯度裁剪
nn.utils.clip_grad_norm_(model.parameters(),[5,10,15])
optimizer.step()


其他优化方法

根据特定的问题,使用分类模型进行训练,然后再训练单独的回个该为题的为模型
比如询问名字,可以使用fasttext先进行意图识别,命中询问名字分类后,直接返回名字
或者是手动构造和名字相关的很多问题,来进行训练,从而能够更加个性化的回答出结果
直接对现有的语料进行修改和清洗,把语料中更多的答案进行替换,比如咨询名字的,咨询天气的等,这样能够更大程度上的回答出更加规范的答案
使用2.4 会讲的搜索模型,不再使用这种生成模型

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值