Skip to content

采样参数 ​

标签
AI/llm
字数
3696 字
阅读时间
15 分钟

自回归解码每一步输出的是整个词表上的概率分布——而词表可以有 151K 个 token。怎么从这个分布里挑出下一个词,就是采样参数干的事。

它们只影响解码阶段,不改变模型的内部表示或权重。所以同一份权重配不同参数,行为可以差很多——这正是「本地自托管和 API 输出不一样」的一个常见原因。

温度:重塑整个分布 ​

温度在 softmax 之前缩放 logits:

p(token)=softmax(logitsT)
温度效果
T = 0等价于贪心——总取最高概率那个
T = 1从模型的自然分布采样
T < 1分布变尖锐——大值更突出,输出更可预测、更连贯,但可能重复、缺乏创造力
T > 1分布变平坦——更有创意,但会不连贯或出现事实错误

实践边界:超过约 1.3 输出通常就退化成不连贯了。

三种「切候选池」的方式 ​

温度是重塑整个分布;下面这三种是把分布切到一个候选池再采样——无论骰子怎么掷,模型都不可能选到极端不合理的 token。

方法机制问题
Top-k只保留概率最高的 k 个,丢弃其余,重新归一化再采样不自适应。模型很自信时 k=40 会保留 39 个几乎无关的 token;模型不确定、有几百个合理候选时 k=40 又砍掉了真实候选
Top-p(nucleus)保留累积概率达到 p 的最小集合,然后只从这个集合里采样池子随模型置信度自适应:自信时几个 token 就够,不确定时需要很多
Min-p设一个相对于最高 token 的概率下限:保留所有至少是最高概率那个的某个倍数的 token同样自适应,但更容易推理,而且不会像固定 top-p 阈值那样让一条平坦的长尾溜进候选池

为什么 top-p 成了多数聊天 API 的默认:它的自适应机制正好匹配「模型有时候很确定、有时候很不确定」这个现实。

三者的具体行为(一组对照) ​

以 logits = [0.6, 0.3, 0.05, 0.03, 0.015, 0.005] 为例:

参数结果问题
Top-k = 4[0.6, 0.3, 0.05, 0.03, 0, 0]Top-k 可能选到概率很低的样本
Top-p = 0.95[0.6, 0.3, 0.05, 0, 0, 0]——
Min-p = 0.5[0.6, 0.3, 0, 0, 0, 0]——

反过来看各自的失效场景:

  • Top-k:logits = [0.6, 0.3, 0.05, 0.03, ...],k=3 → 0.05 概率的 token 也被采进去了
  • Top-p:logits = [0.2, 0.2, 0.2, 0.2, 0.1, 0.1],p=0.95 → 全部值都有效,池子等于没切
  • Min-p:logits = [0.8, 0.1, 0.05, ...],p=0.5 → 只剩一个 token

所以生产上通常是混用的,而不是三选一。

同一组 logits 上,三种切法的边界落点不同。

logits = [0.6, 0.3, 0.05, 0.03, 0.015, 0.005],按概率降序排列

规则          0.6     0.3    0.05    0.03   0.015   0.005
               │       │       │       │       │       │
Top-k = 4      █───────█───────█───────█       ▏       ▏     ← 固定保留 4 个
               │       │       │       │       │       │
Top-p = 0.95   █───────█───────█       │       │       │     ← 累积 0.6+0.3+0.05 = 0.95
               │       │       │       │       │       │
Min-p = 0.5    █───────█       │       │       │       │     ← 概率 ≥ 0.6 × 0.5 = 0.3
               │       │       │       │       │       │
              留      留      留      丢      丢      丢

三种各自的失效场景
Top-k  logits = [0.6, 0.3, 0.05, 0.03, …],k = 3 → 0.05 的 token 也被采进去
Top-p  logits = [0.2, 0.2, 0.2, 0.2, 0.1, 0.1],p = 0.95 → 全部值都有效,池子等于没切
Min-p  logits = [0.8, 0.1, 0.05, …],倍数 0.5 → 只剩一个 token

一条必须知道的交互 ​

温度先于 top-p / top-k 应用。 这意味着低温度会先把分布变尖,进而缩小 top-p 的有效 nucleus。

具体量级:在温度 0.3 + top-p 0.9 下,nucleus 通常只含 1–3 个 token。

这条解释了为什么「调低温度」和「调低 top-p」不是两种等价的做法——它们的作用顺序决定了叠加后的效果。

生产 API 的顺序通常是:temperature → top-k → top-p(HuggingFace 的 _get_logits_warper 就是这个次序;min-p 排在最后),即它们是叠加而不是互斥选项。

一个常见的组合是 top_p=0.9 + temperature=0.7 且不显式设 top-k,让 nucleus 做自适应的工作。

反过来,也有人设 top-k=50 + top-p=0.9——top-k 在这里当安全边界用,防止分布很平时 nucleus 病态地变大。

top-p 太小的副作用:低于 0.6 会导致重复,因为 nucleus 太小,模型反复从一小组 token 里采。

五种对 logits 的调整 ​

前面几种是「切池子」,还有几类是直接改 logits 值。

三种惩罚 ​

惩罚机制默认值 / 范围
频率惩罚(frequency)按已出现次数成比例扣减——用了 5 次的词比用 1 次的被罚得重得多0,[-2, 2]
存在惩罚(presence)只要出现过就扣一个固定值,不论次数多少 —— 推向引入新话题而非只避免字面重复0,[-2, 2]
重复惩罚(repetition)乘法:对已出现 token 的 logit 除以(正值)或乘以(负值)一个因子1.0,[0, 2]

重复惩罚的具体算法:

logits[i] -= frequency_penalty × count
logits[i] -= presence_penalty
logits[i] /= repetition_penalty ** (count - 1)    当 logits[i] > 0
logits[i] *= repetition_penalty ** (count - 1)    当 logits[i] < 0

注意最后两行的方向是反的——正 logit 除以因子会变小,负 logit 乘以因子会更负。两者都是「降低被选中的概率」,方向和量级要一起看才正确。

重复惩罚的副作用:超过 1.5 往往会过度校正,让模型回避本来合理、确实需要重复出现的词。1.2–1.4 是开放式生成常用的区间。

还有两种 ​

操作作用
偏置(logit bias)给指定 token 加一个固定偏移——用于施加硬约束(例:只允许输出 7 个星期名)
掩膜(mask)把范围外的值置为 0——结构化输出的底层机制

操作顺序会改变结果,而且各实现不同 ​

有些实现把惩罚放在温度之前,有些放在之后——同样的参数会产生略有不同的输出分布。

这是自托管模型与 API 端点输出不同的一个原因(即使权重完全一样)。所以复现别人的结果时,除了权重和参数,还要确认采样管线的顺序。

一条 logits 从模型输出到被采成一个 token,中间要过一整条算子链。

logits(整词表,151K 维)
   │
   ├─▶ 惩罚(直接改 logits 值)
   │     frequency :减去 frequency_penalty × count
   │     presence  :只要出现过就减去一个固定值,不论次数
   │     repetition:除以 / 乘以 repetition_penalty^(count−1),正负 logit 方向相反
   │
   ├─▶ temperature:logits / T          ← 重塑整个分布
   │
   ├─▶ top-k:只保留概率最高的 k 个      ┐
   ├─▶ top-p:只保留累积概率达 p 的最小集合 ├─ 切候选池(不改分布形状)
   ├─▶ min-p:只保留 ≥ 最高概率 × 倍数的  ┘
   │
   └─▶ softmax 归一 ──▶ 采样一个 token

各实现的差异都在链上的两处:惩罚放在温度之前还是之后、top-k 与 top-p 谁先。
同样的参数因此会产出略有不同的分布。

确定性采样 ​

对应「不需要多样性、只要更准」的场景(机器翻译、摘要生成)。

贪心:单步最优,不是全局最优 ​

贪心保证每一步取概率最大的,但不保证累积概率最大。 一个具体例子:

路径累积概率
step1 选「吃」(0.6) → step2 选「你」(0.3)0.18
step1 选「爱」(0.3) → step2 选「吃」(0.7)0.21

前缀一变,下一步的分布就变——所以单步贪心会错过累积概率更高的路径。

Beam Search:贪婪与暴力之间的折中 ​

要找全局最优,本质是一个多叉树最优解问题。暴力搜索的路径数是 nk(n 是词表大小,k 是推导步数),单步推理成本极高且 n 通常超过 100K,完全不可行。

压缩 ni 这一项就得到两种方案:

ni方案
= 1贪心搜索(一条路径)
= 定值 kBeam Search(k 条路径)

Beam Search 的步骤:首轮 top-k → 每个分支各自 top-k 并算累积概率 → 全局 k×k 里取前 k → 重复到全部分支遇到结束符 → 取累积概率最大的分支。

它的缺陷与修正:因为概率都在 (0,1) 区间,同一分支计算次数越少,累积概率越高——这让 beam search 趋向于选短输出。解法是长度惩罚:

logprob=cumulative_logprobseq_lenlength_penalty

也可以用 min_new_tokens / max_new_tokens 控制长度范围(实现方式是:长度小于 min_new_tokens 时不让模型输出终止符)。

Beam Search 的扩展与剪枝可以用一棵 k 叉树的层序来读。

k = 2 的示意

步骤 0          步骤 1                        步骤 2
                   ┌─ a (0.6) ─┬─ a·x (0.42)
                   │           └─ a·y (0.18)
   [起始] ─────────┤
                   │           ┌─ b·u (0.21)
                   └─ b (0.3) ─┤
                               └─ b·v (0.09)

第一轮:从整词表取 top-k                  → 得到 k 条候选路径
第二轮:每个分支各自取 top-k(共 k×k 个)  → 全局取前 k,其余整支连同后续剪掉
重复直到全部分支遇到结束符               → 在 k 条完整路径里取累积概率最大的一条

长度惩罚:概率都在 (0, 1) 区间,同一分支算的次数越少累积概率越高,
         beam search 因此天然偏向短输出
         logprob = cumulative_logprob / seq_len^length_penalty

按任务配参数 ​

这是这一篇最有用的一张表——因为最常见的错误就是在所有任务上用同一套参数:

场景temperaturetop-p说明
函数调用 / JSON 提取0——用贪心,不需要采样参数
代码生成0–0.21.0任务有正确答案,多样性是负面的
结构化输出(JSON)01.0贪心避免格式错误
事实问答0–0.30.95低创造
IDE 代码补全0.2–0.40.95比聊天低,保持接地
指令跟随聊天0.70.9不另设 top-k
创意写作0.7–1.00.95需要更高多样性
头脑风暴1.0–1.31.0最大多样性

这张表对 Agent 的直接含义:工具调用必须接近贪心。

同一个智能体里,「决定调哪个工具、参数填什么」和「生成给用户看的自然语言」是两种需求相反的任务——前者要求确定性,后者要求自然。用一个参数跑完全流程,必然有一端是错的。

生产系统应该按端点区分配置,而不是全局一套。

流式的一些实现细节 ​

这些细节会在排查「输出乱码 / 断字」这类问题时用上:

点说明
去分词(detokenization)模型生成的是 token ID 而不是文本。token 边界不与字符边界对齐(尤其多字节 UTF-8),而且一个 token 可能是另一个 token 的前缀——所以服务器有时候要缓冲 1–2 个 token 才能保证去分词后的文本是合法 UTF-8
特殊 token特殊 token 要在输出里过滤掉
token 分组有些提供商会把多个 token 批进一个 SSE 事件——100 token/s 的服务器可能每 30–50 ms 发一个含 3–5 个 token 的事件
背压客户端读得慢时服务器输出缓冲会填满。实现良好的推理服务器会对 decode 循环施加背压——不会生成得比客户端消费得更快,代价是慢客户端拿到更低吞吐

「缓冲 1–2 个 token」这条解释了流式输出里偶尔出现的「字卡一下」——那不是网络问题,是去分词器在等够字节。

流式输出从 token ID 到客户端拿到文本,中间有几处缓冲。

decode 循环
   │ 产出的是 token ID,不是文本
   ▼
去分词缓冲(1–2 个 token)
   │  为什么必须缓冲:token 边界不与字符边界对齐(多字节 UTF-8),
   │  而且一个 token 可能是另一个 token 的前缀
   │  —— 攒够字节才能保证拼出来的文本是合法 UTF-8
   ▼
过滤特殊 token
   │
   ▼
按 token 分组打包
   │  100 token/s 的服务器常每 30–50 ms 发一个含 3–5 个 token 的 SSE 事件
   ▼
SSE 事件 ──▶ 客户端

背压:客户端读得慢 → 输出缓冲填满 → 服务器对 decode 循环施加背压
      不会生成得比客户端消费得更快,代价是慢客户端拿到更低吞吐

和相邻概念的关系 ​

概念关系
温度 vs 幻觉随机采样会偶尔选到「不太准确但很可能」的 token 并引发一连串错误(见 07-模型幻觉)。但不连贯 ≠ 幻觉——前者是话说不通,后者是说得很通但内容假
温度 vs 一致性检测Self-Consistency 和 置信度分层都靠多次采样的一致性当信号——温度太低会削弱这个信号(每次都一样,无从比较);太高则会把「模型不确定」和「采样噪声」混在一起
惩罚 vs 循环重复惩罚是压循环的手段,但它是概率性的;Agent Loop 里的「重复动作检测」是确定性的(同一动作+同一参数连续出现就中断)

相关 ​

参考 ​

贡献者 ​

文件历史 ​