beam search、top-k sampling、nucleus sampling、temperature sampling和联合采样

这几种解码策略在hugging face的GenerationMixin(transformers/generation/utils.py)中均有所实现,在hugging face上的生成式模型都要继承GenerationMixin,以beamsearch为例,下面self就是继承的子类提供的根据w_{0..i-1}给w_{i}打分的language model,这个language model里当然要实现例如kv_cache等策略:

            model_inputs = self.prepare_inputs_for_generation(input_ids, **model_kwargs)

            outputs = self(
                **model_inputs,
                return_dict=True,
                output_attentions=output_attentions,
                output_hidden_states=output_hidden_states,
            )

假设一个搜索任务

假设现在有一个简化版的中文翻译英文任务,输入和输出如下,为了方便描述搜索算法,限制输出词典只有{"I", "H", "U"} 这3个候选词,限制1个时间步长翻译1个汉字,1个汉字对应1个英文单词,这里总共3个汉字,所以只有3个时间步长。

中文输入:"我" "恨" "你"
英文输出:"I" "H" "U"

目标:得到最优的翻译序列 I-H-U

exhaustive search(穷举搜索)

最直观的方法就是穷举所有可能的输出序列,3个时间步长,每个步长3种选择,共计  种排列组合。

I-I-I
I-I-H
I-I-U
I-H-I
I-H-H
I-H-U
I-U-I
I-U-H
I-U-U

H-I-I
H-I-H
H-I-U
H-H-I
H-H-H
H-H-U
H-U-I
H-U-H
H-U-U

U-I-I
U-I-H
U-I-U
U-H-I
U-H-H
U-H-U
U-U-I
U-U-H
U-U-U

从所有的排列组合中找到输出条件概率最大的序列。穷举搜索能保证全局最优,但计算复杂度太高,当输出词典稍微大一点根本无法使用。

greedy search(贪心搜索)

贪心算法在翻译每个字的时候,直接选择条件概率最大的候选值作为当前最优。如下图所以,

  • 第1个时间步长:首先翻译"我",发现候选"I"的条件概率最大为0.6,所以第一个步长直接翻译成了"I"。
  • 第2个时间步长:翻译"我恨",发现II概率0.2,IH概率0.7,IU概率0.1,所以选择IH作为当前步长最优翻译结果。
  • 第3个时间步长:翻译"我恨你",发现IHI概率0.05,IHH概率0.05,IHU概率0.9,所以选择IHU作为最终的翻译结果。

PS:图中的概率如何得来的?不同的模型有不同的算法,我自己随便填的。

greedy search

贪心算法每一步选择中都采取在当前状态下最好或最优的选择,通过这种局部最优策略期望产生全局最优解。但是期望是好的,能不能实现是另外一回事了。贪心算法本质上没有从整体最优上加以考虑,并不能保证最终的结果一定是全局最优的。但是相对穷举搜索,搜索效率大大提升。

上个伪代码:

def greedy_decoding(input_ids, max_tokens=300):
    with torch.inference_mode():
        for _ in range(max_tokens):
            outputs = model(input_ids)
            next_token_logits = outputs.logits[:, -1, :]
            next_token = torch.argmax(next_token_logits, dim=-1)
        if next_token == tokenizer.eos_token_id:
            break
        input_ids = torch.cat([input_ids, rearrange(next_token, 'c -> 1 c')], dim=-1)
        generated_text = tokenizer.decode(input_ids[0])
        return generated_text

beam search(束搜索)

简单来说就是每一轮存beam size个候选的句子,这beam size个候选的句子可以得到beam size*vocab size个候选词,在beam size*vocab size个候选词中再选择beam size个继续进入下一轮。

beam search是对greedy search的一个改进算法。相对greedy search扩大了搜索空间,但远远不及穷举搜索指数级的搜索空间,是二者的一个折中方案。

beam search有一个超参数beam size(束宽),设为k。第一个时间步长,选取当前条件概率最大的k个词,当做候选输出序列的第一个词。之后的每个时间步长,基于上个步长的输出序列,挑选出所有组合中条件概率最大的k个,作为该时间步长下的候选输出序列。始终保持k个候选。最后从k个候选中挑出最优的。

还是以上面的任务为例,假设k=2,我们走一遍这个搜索流程。

  • 第一个时间步长:如下图所示,I和H的概率是top2,所以第一个时间步长的输出的候选是I和H,将I和H加入到候选输出序列中。

beam search 第一个时间步长

  • 第2个时间步长:如下图所示,以I开头有三种候选{II, IH, IU},以H开头有三种候选{HI, HH, HU}。从这6个候选中挑出条件概率最大的2个,即IH和HI,作为候选输出序列。

beam search 第二个时间步长

  • 第3个时间步长:同理,以IH开头有三种候选{IHI, IHH, IHU},以HI开头有三种候选{HII, HIH, HIU}。从这6个候选中挑出条件概率最大的2个,即IHH和HIU,作为候选输出序列。因为3个步长就结束了,直接从IHH和IHU中挑选出最优值IHU作为最终的输出序列。

beam search 第三个时间步长

  • beam search不保证全局最优,但是比greedy search搜索空间更大,一般结果比greedy search要好。
  • greedy search 可以看做是 beam size = 1时的 beam search。
 
import torch
import torch.nn.functional as F
 
def beam_search(LM_prob,beam_size = 3):
    batch,seqlen,vocab_size = LM_prob.shape
    # 对LM_prob取对数,LM_prob: [batch,seqlen,vocab_size]
    # 这个LM_prob不好的地方在于把LM生成的seqlen都放到了LM_prob这一个tensor里了,seqlen的每一个长度代表一次结果,而vocab_size注意不是embedding,代表是vocab里哪个词
    log_LM_prob = LM_prob.log()
    #先选择第0个位置的最大beam_size个token,log_beam_prob与indices的shape为[batch,beam_size]
    log_beam_prob, indices = log_LM_prob[:,0,:].topk(beam_size,sorted = True)
    # topk这个函数刚好是batch内每个元素中所有beam_size之中选topk,log_beam_prob的shape是[batch,beam_size]
    indices = indices.unsqueeze(-1) # indices的shape是[batch,beam_size,1]
    #对每个长度进行beam search
    for i in range(1,seqlen):
        #每个beam的可能产生的概率,由于是log的概率可以直接加
        # log_beam_prob.unsqueeze(-1)的shape是[batch,beam_size,1]
        # log_LM_prob[:,i,:].unsqueeze(1).repeat(1,beam_size,1)的shape是[batch,beam_size,vocab_size]
        log_beam_prob = log_beam_prob.unsqueeze(-1) + log_LM_prob[:,i,:].unsqueeze(1).repeat(1,beam_size,1)
        #选择当前步概率最高的token,此时的log_beam_prob的shape是[batch,beam_size,vocab_size]
        log_beam_prob, index = log_beam_prob.view(batch,-1).topk(beam_size,sorted = True)
        #下面的计算:beam_id选出新beam来源于之前的哪个beam;index代表真实的token id
        #beam_id,index的shape是[batch,beam_size]
        beam_id = index//vocab_size
        index = index%vocab_size
        mid = torch.Tensor([])
        #对batch内每个样本循环,选出beam的同时拼接上新生成的token id
        for j,bid,idx in zip(range(batch),beam_id,index):
            x = torch.cat([indices[j][bid],idx.unsqueeze(-1)], -1) # x.shape是[beam_size, cur_seq_len]
            mid = torch.cat([mid,x.unsqueeze(0)], 0) # mid这时候被拼成[cur_batch, beam_size, cur_seq_len]
        indices = mid #这时候的indices.shape是[batch, beam_size, cur_seq_len]
        print('indices.shape={}'.format(indices.shape))
    return indices, log_beam_prob
 
if __name__=='__main__':
    # 建立一个语言模型 LM_prob (batch,seqlen,vocab_size),这个纯属偷懒,相当于省去每次decode的工作
    # (batch_i,seqlen_i,vocab_size_i)表示第batch_i个batch中第
    LM_prob = F.softmax(torch.randn([32,20,1000]),dim = -1)
    #最终返回每个候选,以及每个候选的log_prob,shape为(batch,beam_size,seqlen)
    indices,log_prob = beam_search(LM_prob,beam_size = 3)
    # print(indices)

Top-K sampling

简单说beam search的缺点是high quality human language does not follow a distribution of high probability next words,需要一定的随机性,所以有了采样。这就是top-k sampling:在解码的每个时间步从前k个概率最大的词中按它们的概率进行采样。但top-k sampling中k的选择是个难题,选大了可能会采样出长尾词,导致语句不通顺,选小了又退化成了Beam Search。

def top_k_sampling(input_ids, max_tokens=100, top_k=50, temperature=1.0):
    for _ in range(max_tokens):
    with torch.inference_mode():
        outputs = model(input_ids)
        next_token_logits = outputs.logits[:, -1, :]
        top_k_logits, top_k_indices = torch.topk(next_token_logits, top_k)
        top_k_probs = F.softmax(top_k_logits / temperature, dim=-1)
        next_token_index = torch.multinomial(top_k_probs, num_samples=1)
        next_token = top_k_indices.gather(-1, next_token_index)
        input_ids = torch.cat([input_ids, next_token], dim=-1)
        generated_text = tokenizer.decode(input_ids[0])
    return generated_text

Nucleus sampling(top-p sampling)

简单说就是把上面top-k sampling里top k个词换成了top p的概率分布,在每个时间步,头部的几个词的出现概率已经占据了绝大部分概率空间,把这部分核心词叫做nucleus,这个名字起得有点唬人,叫Core Sampling可能更直观些 (但不fancy)

def top_p_sampling(input_ids, max_tokens=100, top_p=0.95):
    with torch.inference_mode():
        for _ in range(max_tokens):
            outputs = model(input_ids)
            next_token_logits = outputs.logits[:, -1, :]
            sorted_logits, sorted_indices = torch.sort(next_token_logits, descending=True)
            sorted_probabilities = F.softmax(sorted_logits, dim=-1) 
            cumulative_probs = torch.cumsum(sorted_probabilities, dim=-1)
            sorted_indices_to_remove = cumulative_probs > top_p
            sorted_indices_to_remove[..., 0] = False 
            indices_to_remove = sorted_indices[sorted_indices_to_remove]
            next_token_logits.scatter_(-1, indices_to_remove[None, :], float('-inf'))
            probs = F.softmax(next_token_logits, dim=-1)
            next_token = torch.multinomial(probs, num_samples=1)
            input_ids = torch.cat([input_ids, next_token], dim=-1)
        generated_text = tokenizer.decode(input_ids[0])
    return generated_text

Temperature sampling

本质上就是在 Softmax 函数上添加了温度(T)这个参数,以下截图自 大模型文本生成——解码策略(Top-k & Top-p & Temperature) - 知乎

import torch
import torch.nn.functional as F

def temperature_sampling(logits, temperature=1.0):
    logits = logits / temperature
    probabilities = F.softmax(logits, dim=-1)
    sampled_token = torch.multinomial(probabilities, 1)
    return sampled_token.item()

联合采样(top-k & top-p & Temperature)

通常我们是将 top-k、top-p、Temperature 联合起来使用。使用的先后顺序是 top-k->top-p->Temperature。

我们还是以前面的例子为例。

首先我们设置 top-k = 3,表示保留概率最高的3个 token。这样就会保留女孩、鞋子、大象这3个 token。

  • 女孩:0.664
  • 鞋子:0.199
  • 大象:0.105

接下来,我们可以使用 top-p 的方法,保留概率的累计和达到 0.8 的单词,也就是选取女孩和鞋子这两个 token。接着我们使用 Temperature = 0.7 进行归一化,变成:

  • 女孩:0.660
  • 鞋子:0.340

接着,我们可以从上述分布中进行随机采样,选取一个单词作为最终的生成结果。

部分转载自:

1. Nucleus Sampling与不同解码策略简介 - 知乎

2. 来自hugging face的博客,比较长但是说的比较细:How to generate text: using different decoding methods for language generation with Transformers3. 大模型文本生成——解码策略(Top-k & Top-p & Temperature) - 知乎

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值