第4章:Top-p(Nucleus)采样:原理与动态截断

聊完top-k采样,咱们再看一个更聪明的招——top-p采样。

说实话,我用top-k那段时间,最头疼的就是那个k值。k=40?50?场景一变,效果就飘了。有一次我在做对话生成,同一个prompt,第一次回复流畅,第二次就蹦出一个生僻词,直接把对话带偏了。

后来我想,能不能让模型自己决定选多少个词?这就是top-p干的事。

4.1 核心思想:累积概率截止

top-p采样不固定选几个词。它先对概率排序,然后从高到低累加。

累加到一个阈值p的时候,停。剩下的词都不要。

你想想看,这个p控制的是“质量的底线”。

  • p=0.9:保留累积概率占90%的头几个词。
  • p=0.95:放宽一点,允许更多词进入候选池。
  • p=1.0:基本上退化成完整概率采样,不做任何截断。
我的经验:p值选0.92到0.96之间,大多数文本任务都不会翻车。我一般从0.95开始试,不够多样就降到0.9,太跳跃就升到0.97。

4.2 Top-p vs Top-k:亲兄弟,但理念完全不同

两者都是截断策略,但思路正好反过来。

特性 Top-k 采样 Top-p 采样
截断方式 固定数量 动态数量
候选词数 始终k个 根据p值变化
概率分布平坦时 可能漏掉合理词(k太小) 会自动包含更多词
概率分布尖锐时 可能包含多余的低概率词(k太大) 只保留核心词
参数调优难度 需要针对每个场景调整 p值一般一次调好就能通用

我有一个很直观的感受:top-k像是一条固定宽度的河,水流急时河床不够宽,水流缓时又太宽;top-p则像是能自适应调节的河道,水位高就拓宽,水位低就收窄。

4.3 p值怎么选?

选p值,核心是看你想让模型多“敢说”。

  • 高p值(0.95~1.0):多样性高,但容易跑偏。适合故事生成、对话系统。
  • 中p值(0.8~0.95):平衡点。大多数场景都行。
  • 低p值(0.6~0.8):稳,但可能无聊。适合机器翻译、摘要生成。
避坑指南:我曾经把p值降到0.6跑一个创意写作任务,结果模型每句话都挑最普通的词,输出干巴巴得像说明书。后来我明白,p值越低,候选词越少,文本创造力越受限。

4.4 动态截断:在不同位置有不同的表现

为什么叫“动态截断”?

因为同一个p值,在不同位置截出的词数不一样。

我举个例子,假设句子开头是“我今天去”,模型觉得这是一个常见搭配,所有候选词加起来可能不超过5个。但句子变成“我突然发现”,概率分布就更分散,可能需要10个甚至更多词才能累积到0.9。

def top_p_filtering(logits, p=0.9):
    sorted_logits, sorted_indices = torch.sort(logits, descending=True)
    cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)

    # 找到累积概率超过p的位置
    sorted_indices_to_remove = cumulative_probs > p

    # 至少要保留第一个词,否则可能全切掉
    sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
    sorted_indices_to_remove[..., 0] = 0

    indices_to_remove = sorted_indices[sorted_indices_to_remove]
    logits[..., indices_to_remove] = -float('Inf')
    return logits
关键点:上面的代码里有一个小细节,我在几个项目里都踩过坑——sorted_indices_to_remove[..., 1:] = ...这一行。如果不处理,累积概率刚好等于p的那个词也会被切掉,造成截断偏紧。必须确保至少保留第一个词。

4.5 实践中的调整技巧

选p值不能光看理论,得结合场景跑几轮才知道。

  • 小批量试跑:用50个prompt,p值从0.85到0.95,每步0.02,看输出质量。
  • 关注重复率:p值太大容易重复,尤其是对同一段的结尾。
  • 注意低概率词突现:如果突然出现一个完全无关的词,检查p值是否太高。
  • 长期依赖问题:越到序列靠后,模型概率分布越尖锐。这时候p值不变,但候选词数自动减少,反而是好事。

4.6 组合使用:Top-p + Top-k

嗯,这里要注意一个实战技巧。我从来不会只用top-p或者top-k,我会两个一起用。

先top-k切到一个规模可控的范围(比如k=100),再用top-p精挑细选。

这样做的优点是:top-k做了第一道粗筛,避免概率分布尾部那些没意义的长尾词进入候选堆;top-p再做细筛,挑出真正合适的词。

我的习惯用法:k=100, p=0.95。先粗筛一千到一万个词到一百个,再从一百个里挑累积概率0.95的词。这两个参数组合,基本覆盖了我见过的所有文本生成场景。
def combined_sampling(logits, k=100, p=0.95):
    # 第一步:top-k粗筛
    top_k_values, _ = torch.topk(logits, k=k)
    min_top_k = top_k_values[..., -1, None]
    logits[logits < min_top_k] = -float('Inf')

    # 第二步:top-p细筛
    sorted_logits, sorted_indices = torch.sort(logits, descending=True)
    cumulative_probs = torch.cumsum(F.softmax(sorted_logits, dim=-1), dim=-1)
    sorted_indices_to_remove = cumulative_probs > p
    sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
    sorted_indices_to_remove[..., 0] = 0

    indices_to_remove = sorted_indices[sorted_indices_to_remove]
    logits[..., indices_to_remove] = -float('Inf')
    return logits
别忘了温度:top-p和top-k都对概率分布敏感。我看到很多同学调半天参数没效果,其实忘了先调温度。先把温度校准到0.8~1.2之间,再调p值,顺序不能反。
Top-p 采样流程与动态截断 输入 Logits Softmax 概率归一化 概率降序排序 累积概率计算 找到 累积概率 ≥ p 的临界点 截断:保留临界点 之前的所有词 不同上下文下,同一个 p 值截出的词数自动变化,实现动态截断

以上就是top-p采样的全部内容。

p值选好了,你的模型输出稳定性会提升一大截。我自己现在不管做什么生成任务,都默认从p=0.95开始调,配合一个k=100的粗筛,很少翻车。