模型每生成一个 token,前向传播给出的并不是一个词,而是整个词表上的一个概率分布。从这个分布里"挑"出下一个 token 的规则,就是采样策略。temperaturetop_ktop_p 这几个最常被调的参数,控制的全是这最后一步。它们不改变模型本身,却直接决定输出是刻板复读还是天马行空。搞懂它们,就能精准地在"准确稳定"和"多样有创意"之间下旋钮。

直觉:从 logits 到一个被采样的 token

最后一层输出的是每个 token 的原始分数,叫 logits,记作 zRVz \in \mathbb{R}^VVV 是词表大小)。logits 经 softmax 变成概率:

pi=ezij=1Vezjp_i = \frac{e^{z_i}}{\sum_{j=1}^{V} e^{z_j}}

拿到分布 pp 后有两条极端路线:

  • 贪心解码(greedy):每步直接取概率最大的 token(argmax\arg\max)。确定、可复现,但容易陷入重复、刻板,缺乏多样性。
  • 纯随机采样:严格按 pp 掷骰子。多样,但长尾里那些概率极低的离谱 token 也有非零机会被选中,偶尔蹦出语无伦次的内容。

实践中我们要的是中间地带:可调地控制随机性的强弱,并裁掉危险的长尾。temperature 管前者,top-k / top-p 管后者。

temperature:缩放分布的"陡峭程度"

temperature TT 在 softmax 之前对 logits 做缩放:

pi=ezi/Tjezj/Tp_i = \frac{e^{z_i / T}}{\sum_j e^{z_j / T}}

直觉是它重塑分布的陡峭/平坦

  • T0T \to 0:放大 logits 差距,概率质量集中到最大那个 token,退化为贪心。输出确定、保守。
  • T=1T = 1:用模型原始分布,不做缩放。
  • T>1T > 1:拉平分布,低概率 token 的相对机会上升,输出更随机、更"发散"。

注意 temperature 是单调缩放——它改变各 token 的相对概率,但不改变排序(最可能的依然最可能)。它调的是"愿意冒多大险偏离最优"。

经验取值:事实问答、代码、抽取这类要确定性的任务用 T[0,0.3]T \in [0, 0.3];常规对话 0.7\approx 0.7;写诗、头脑风暴等要多样性的可上探到 11 附近甚至更高。但 TT 过大会失控:把概率拉得太平,长尾噪声 token 被采中的风险陡增——这正是需要 top-k / top-p 兜底的原因。

top-k:只在前 k 个候选里采样

top-k 的规则朴素:只保留概率最高的 kk 个 token,其余全部置零,在这 kk 个里重新归一化后采样。它从源头掐掉了长尾里的离谱选项。

1
2
3
4
def top_k_filter(logits, k):
# 保留 top-k,其余设为 -inf,softmax 后即为 0
kth_value = torch.topk(logits, k).values[..., -1, None]
return torch.where(logits < kth_value, float('-inf'), logits)

它的硬伤是 kk 固定,但分布的"陡峭度"是变化的

  • 当模型很笃定(分布尖锐,比如代码里 ) 后面几乎必然换行),前 5 个候选可能已经覆盖 99% 概率,k=50k=50 会强行纳入 45 个本该忽略的噪声。
  • 当模型很犹豫(分布平坦,比如开放式叙事的下一句),合理候选可能有上百个,k=50k=50 又把不少正当选项砍掉了。

一个固定的 kk 没法同时适配这两种局面。这正是 top-p 想解决的。

top-p(nucleus):按累积概率动态截断

top-p 不固定候选个数,而是固定累积概率质量 pp:把 token 按概率从高到低排序,从前往后累加,刚好凑够 pp 的最小集合(称为 nucleus)就停,只在这个集合里采样。

1
2
3
4
5
6
7
8
9
def top_p_filter(logits, p):
sorted_logits, sorted_idx = torch.sort(logits, descending=True)
cum_probs = torch.cumsum(softmax(sorted_logits, dim=-1), dim=-1)
# 累积超过 p 之后的 token 全部丢弃(保留刚好越过阈值的那个)
remove = cum_probs > p
remove[..., 1:] = remove[..., :-1].clone()
remove[..., 0] = False
sorted_logits[remove] = float('-inf')
return sorted_logits.scatter(-1, sorted_idx, sorted_logits) # 还原顺序

它的妙处是候选集大小随模型置信度自适应

  • 分布尖锐时,少数几个 token 就累积到 pp,nucleus 很小,输出贴近确定。
  • 分布平坦时,需要更多 token 才凑够 pp,nucleus 自动变大,保留了应有的多样性。

常用 p[0.9,0.95]p \in [0.9, 0.95]:意思是"只在覆盖 90%~95% 概率质量的核心候选里采样,剩下 5%~10% 的长尾直接不要"。比起 top-k,它对不同位置的不确定性更友好,是目前更常用的默认裁剪方式。

它们如何组合:一条流水线

这几个参数不是互斥的,实际推理引擎通常串起来按序施加

1
2
3
4
5
6
logits
└─> 除以 temperature # 调随机性强弱(陡峭/平坦)
└─> top-k 截断 # 砍到前 k 个(可选)
└─> top-p 截断 # 再按累积概率收紧到 nucleus
└─> softmax 归一化
└─> 按概率采样一个 token

顺序值得留意:先用 temperature 重塑分布,截断。如果先截断再升温,被砍掉的 token 永远回不来;先升温会改变各 token 相对概率,进而影响 top-p 算出的 nucleus 边界。多数实现按"temperature → top-k → top-p"的顺序,结果是:temperature 定"冒险程度",top-k/top-p 定"安全护栏",二者职责正交、互补。

工程权衡与常见误区

  • temperature=0 不保证 100% 可复现。它确实退化为贪心,但浮点运算的非确定性、并行归约顺序、不同 batch 下的 kernel 选择,仍可能让同一输入产生细微差异。要严格复现还需固定随机种子、后端配置等。
  • 别同时把 temperature 和 top-p 都开很大。高温把分布拉平、top-p 又放宽护栏,长尾噪声叠加,容易输出离题甚至语无伦次。要发散通常调一个就够。
  • 采样治不了知识缺口。把 TT 调到 0 只让错误答案变得稳定可复现,并不会让模型凭空知道它本来不知道的事实。
  • 多样性的正确用法:同一 prompt 用 T>0T>0 采样多条再投票(self-consistency),常比单条贪心更准——这是用受控随机性换取覆盖面,而非盲目调高温度。
  • 重复惩罚是另一族旋钮repetition_penaltyfrequency_penalty 等在采样前对已出现 token 的 logits 做衰减,专治"复读机",和上面的随机性/截断参数正交,可叠加使用。

小结

采样策略全部作用在"分布 → token"这最后一步,不碰模型本身。temperature 调随机性强弱(缩放分布陡峭度,不改排序),top-k 砍到固定数量、top-p 按累积概率自适应裁掉长尾——前者管冒险,后者管护栏,职责互补。把它们当成"准确-稳定 ↔ 多样-创意"这根轴上的连续旋钮:要确定就低温 + 紧 top-p,要发散就升温 + 松护栏。理解了背后的 softmax 与截断逻辑,调参就不再是碰运气,而是有依据的工程决策。