在基于Transformer的大模型推理过程中,生成下一个token的方式并不是简单取概率最大的那个词,而是从softmax输出的概率分布中进行采样。采样策略直接决定了回答的多样性、连贯性以及是否出现重复或跑题。Top-k与Top-p正是两种最常见的截断采样方法,很多工程师在部署时只改温度系数,却忽略了这两个参数对输出分布的深层影响。

采样机制的本质差异:从Softmax分布到截断策略
语言模型每一轮解码都会输出一个logits向量,经过softmax归一化后得到所有词表中每个token的概率分布。如果不做任何截断,直接从完整分布中采样,低概率的长尾词会被频繁选中,导致输出包含大量无意义符号或罕见词。为了避免这种情况,需要在采样前对分布进行裁剪,只保留一部分高质量候选词。
Top-k的做法非常直接:只保留概率最高的k个token,其余token的概率置零,然后重新归一化并采样。这里的k是一个固定整数,比如k=50。这种固定数量的截断在分布尖锐时(概率集中在少数词上)会把一些低概率词强行纳入候选集;而在分布平坦时又可能把真正有价值的词排除在外。换句话说,Top-k无法根据每个时间步的具体分布动态调整候选集大小。
Top-p(也叫核采样)的思路完全不同。它不预设候选词数量,而是从概率最高的词开始累加,直到累计概率达到阈值p(例如p=0.9),把被累加的词作为候选集,其余词截断。这样候选集的大小会随分布的集中程度自动变化:分布越集中,候选词越少;分布越平坦,候选词越多。这种自适应特性使得Top-p在开放文本生成中通常比Top-k更稳定。
Top-k与Top-p的数学定义与代码实现
假设当前步的softmax概率分布为P,词表大小为V。Top-k采样首先选择概率最高的k个token,得到索引集合S_k,然后将这些token的概率重新归一化:P'(i) = P(i) / sum(P(j) for j in S_k),最后从P'中采样。Top-p则是找到最小的候选集S_p,使得sum(P(j) for j in S_p) >= p,同样重新归一化后采样。
下面用PyTorch实现这两个采样函数,方便在推理代码中直接复用。需要注意的是,实际工程中通常会先应用温度缩放(temperature scaling)来调节分布的尖锐程度。
import torch
import torch.nn.functional as F
def sample_top_k(logits, k, temperature=1.0):
# 温度缩放
logits = logits / temperature
# 获取概率最高的k个索引
top_k_logits, top_k_indices = torch.topk(logits, k)
# 重新归一化
probs = F.softmax(top_k_logits, dim=-1)
# 从候选集中采样一个索引
sampled_idx_in_topk = torch.multinomial(probs, num_samples=1)
sampled_idx = top_k_indices.gather(-1, sampled_idx_in_topk)
return sampled_idx
def sample_top_p(logits, p, temperature=1.0):
logits = logits / temperature
# 按概率降序排序
sorted_logits, sorted_indices = torch.sort(logits, descending=True)
# 计算累计概率
sorted_probs = F.softmax(sorted_logits, dim=-1)
cumulative_probs = torch.cumsum(sorted_probs, 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
# 将需要移除的词概率置零
sorted_logits[sorted_indices_to_remove] = -float('inf')
# 重新归一化并采样
probs = F.softmax(sorted_logits, dim=-1)
sampled_idx_in_sorted = torch.multinomial(probs, num_samples=1)
sampled_idx = sorted_indices.gather(-1, sampled_idx_in_sorted)
return sampled_idx
上述实现中,Top-k直接使用torch.topk截取前k个logits,Top-p则需要先排序再计算累计和。从计算复杂度看,Top-p通常比Top-k多一次排序操作,但在现代GPU上这个开销可以忽略。更重要的是,Top-p在分布平坦时会自动扩大候选集,避免了Top-k固定k值带来的僵硬。
对比两者的行为差异,可以举一个极端例子:如果某个时间步的概率分布中前3个词的概率之和已经达到0.98,那么Top-p(p=0.9)只会保留这3个词,而Top-k(k=50)会强行保留50个词,其中包含大量概率接近于零的噪声词。反过来,如果分布非常平坦,前50个词的概率之和可能只有0.6,Top-k会截断掉剩下40%概率的词,而Top-p(p=0.9)会保留更多词直到累计达到0.9。这就是为什么Top-p能更好地适配不同分布形态。
推理场景下的实际表现与调参策略
在开放域对话生成中,我们既希望回答有创意,又希望内容连贯不跑题。Top-p通常优于Top-k,因为它的动态截断机制能保留足够的多样性,同时过滤掉长尾噪声。经验上,p值设置在0.85到0.95之间比较合适,配合温度0.7到0.9,可以生成流畅且自然的回复。如果希望输出更保守,可以降低p值到0.8;如果希望更有创意,可以提高到0.95甚至0.98。
对于代码生成、数学推理等需要精确性的任务,采样策略要更加谨慎。这类任务通常建议使用贪心解码(即temperature=0)或者beam search,而不是随机采样。如果必须使用采样,建议设置较小的top_p(如0.8)和较小的温度(0.2到0.5),同时可以结合top_k=10来进一步限制候选空间。需要注意的是,代码生成中如果使用top_p过高,很容易生成语法错误但看起来合理的代码片段。
翻译任务与摘要任务类似,输出空间相对受约束,但不同语言对之间的分布差异很大。一个实用的策略是先固定top_p=0.9,然后根据验证集的表现微调温度。如果翻译结果出现大量重复词,说明采样过于激进,可以降低temperature或降低p值;如果翻译结果过于保守、用词单一,则可以适度提升temperature。不要同时大幅调整多个参数,否则难以归因。
下面给出一个推理场景参数选择的参考表格,帮助快速定位。
| 任务类型 | 推荐组合 | 说明 |
|---|---|---|
| 开放域闲聊 | temperature=0.8, top_p=0.9 | 平衡多样性与连贯性 |
| 创意写作 | temperature=1.0, top_p=0.95 | 增加随机性与想象力 |
| 代码生成 | temperature=0.3, top_p=0.8, top_k=20 | 降低语法错误风险 |
| 翻译/摘要 | temperature=0.6, top_p=0.85 | 保持语义准确,适度灵活 |
| 数学推理 | greedy或beam search | 禁用随机采样 |
表格中的参数不是绝对标准,不同基座模型(如LLaMA、Qwen、DeepSeek)对采样参数的敏感度不同,建议在验证集上做小范围网格搜索。另外,推理部署时还要考虑延迟约束:Top-p的排序操作在超长词表(比如10万+)上可能略微增加耗时,但相比模型前向传播可忽略不计。如果使用vLLM或TensorRT-LLM等推理框架,这些采样逻辑通常已被CUDA kernel优化,无需担心。
还有一个常见误区:把top_p设置成1.0就等于不截断,完全从原始分布采样,这会导致输出质量急剧下降,因为长尾噪声词被大量选中。同样,top_k设置得过大(比如超过1000)也接近于不截断。在实际推理中,top_p最好不超过0.98,top_k不要超过200,否则截断就失去了意义。