2726 字
约 9 分钟
22
S23-X05 · 采样参数:temperature、top-p 与 top-k

同一个 prompt 发两次,一次干净利落,一次绕圈说废话。有人归因于"模型不稳定",其实多半是采样参数——模型每步输出的是一整条概率分布,采样是从这条分布里抽下一个 token 的规则。

它同时决定三件事:多样性、可复现性、结构化任务能否稳定交付。三件事混在一起调,就永远说不清是哪一步坏的。


1. 采样在解决什么

需求 想要的性质 参数取向
创意写作、头脑风暴 每次不同,敢于走偏 高温 + 宽候选
事实抽取、分类判定 稳定、可复现 贪心或近贪心
代码生成 语法正确优先,允许少量多样 低温 + 停止序列
结构化输出(JSON) 格式零容错 约束解码,参数只做微调
回归测试 同输入必须同输出 固定种子 + 固定参数 + 固定版本

先确认自己在哪一行,再谈参数值。


2. 三个参数的机制

设模型对第 t 步输出 logits z(词表大小 V):

p_i = softmax(z_i / T)                     # temperature 缩放
top-k: 只保留概率最高的 k 个,其余置 0 后重归一
top-p: 按概率降序累加,保留累计 ≥ p 的最小集合,其余置 0 后重归一
参数 作用对象 调大的效果 调小的效果 风险
temperature 整条分布的陡峭度 分布变平,低概率 token 更容易被抽到 分布变尖,趋向 argmax 过高开始语无伦次
top-k 候选集合大小 候选多,多样性升 候选少,趋向安全词 太小会砍掉合理续写
top-p 累计概率阈值 候选多 候选少 分布尖时几乎无效

temperature 改形状,top-k/top-p 改范围。同时设置时,通行顺序是:按 T 缩放 → top-k 截断 → top-p 截断 → 重归一 → 采样。

# 等价实现,便于理解生效顺序(实际由引擎 kernel 完成)
import torch
def sample(logits, T=1.0, k=0, p=1.0, generator=None):
    logits = logits / max(T, 1e-5)
    if k and k > 0:
        kth = torch.topk(logits, k).values[..., -1, None]
        logits = logits.masked_fill(logits < kth, float("-inf"))
    if p < 1.0:
        sorted_logits, idx = torch.sort(logits, descending=True)
        probs = torch.softmax(sorted_logits, dim=-1).cumsum(-1)
        drop = probs - torch.softmax(sorted_logits, dim=-1) > p
        sorted_logits = sorted_logits.masked_fill(drop, float("-inf"))
        logits = torch.full_like(logits, float("-inf")).scatter(-1, idx, sorted_logits)
    probs = torch.softmax(logits, dim=-1)
    return torch.multinomial(probs, 1, generator=generator)

少数实现先 top-p 再 top-k。分布平坦时差异明显,跨引擎迁移参数要按引擎文档核对。

{
  "temperature": 0.7,
  "top_p": 0.95,
  "top_k": 40,
  "max_tokens": 512,
  "stop": ["\n\n用户:", "</answer>"],
  "seed": 20261109
}

同时给三个值不算错,同时改三个再比较结果才是错:无法归因。常见做法是固定 top_p=1.0、top_k=0,只调 temperature;或反过来。


3. temperature=0 不等于确定性

T=0 直觉上就是"永远选最大概率的 token"。它实际只保证每一步的 argmax 在当前浮点值下相同,而浮点值本身会变。

非确定性来源 机制 影响
浮点归约顺序 不同 batch 形状下 GEMM/reduction 顺序不同 logits 末位不同,接近并列时翻转
批处理组成 同一请求在不同批里的位置不同 同上,且难复现
并行策略 TP/PP 切分改变累加顺序 多卡与单卡结果可能有差
引擎版本 kernel 实现变更 升级后输出漂移
前缀缓存命中 命中与未命中的计算路径不同 同一请求两次命中状态不同
# 验证:同一请求发三次,看 finish 与 token 数是否一致
for i in 1 2 3; do
  curl -s http://HOST:8000/v1/chat/completions -H 'Content-Type: application/json' \
    -d '{"model":"MODEL","messages":[{"role":"user","content":"PROMPT"}],
         "temperature":0,"seed":42,"max_tokens":128}' \
    | python3 -c 'import sys,json;d=json.load(sys.stdin);print(d["choices"][0]["finish_reason"], len(d["choices"][0]["message"]["content"]))'
done

三次一致只说明"这次没撞上临界情况"。要更稳,得固定引擎版本、固定 batch 策略、关闭前缀缓存,或接受"近似可复现"并在评测里多次采样取多数。


4. 重复惩罚与副作用

参数 机制 副作用
frequency_penalty 按 token 已出现次数线性扣分 惩罚必要的重复(列表编号、代码缩进、字段名)
presence_penalty 出现过就扣一次固定分 让模型回避主题词,答非所问
repetition_penalty logits 除以惩罚因子 乘法式惩罚,取值稍大就伤语义
{"frequency_penalty": 0.3, "presence_penalty": 0.0, "repetition_penalty": 1.05}

repetition_penalty 是乘法,比线性惩罚猛,1.2 已足以压住代码里的 self、def、return。代码与结构化输出场景建议三个都保持默认:多数"复读"其实是 temperature 太低或停止序列没配。


5. max_tokens 与截断判定

输出被截断时,引擎会在 finish_reason 里写明原因:

finish_reason 含义 处理
stop 命中停止序列 检查停止词是否把正文切了
length 达到 max_tokens 输出不完整,考虑续写或加预算
content_filter 被内容策略截断 换措辞或换路径
tool_calls 转向工具调用 正常路径
r = client.chat.completions.create(model="MODEL", messages=msgs, max_tokens=512)
fin = r.choices[0].finish_reason
if fin == "length":
    # 续写:把已生成内容拼回 messages,注意别重复 system prompt
    msgs = msgs + [{"role": "assistant", "content": r.choices[0].message.content}]

靠加大 max_tokens 治截断通常治不好:真相多是提示词没约束长度、模型自问自答、或停止序列写错。它只是安全阀。


6. 停止序列的正确用法

停止序列是"一出现就切断"的字面匹配,它不理解语义。

用法 例子 注意
分隔多轮模板 "\n\n用户:" 要在模板里真实存在
切代码块结束 "```" 正文里出现代码块就会被切
切结构化标签 "</answer>" 标签未闭合时会切在半截
切工具调用 `"< tool_call

误截断的根因永远是"停止词在正文里合法出现"。做法:选正文不会自然出现的组合(带换行、带模板标记);必须闭合的输出(JSON)改用约束解码。


7. 结构化输出:低温还是约束解码

方案 保证程度 代价
低温 + 提示词要求 JSON 概率性,偶发破损 无额外开销
JSON mode(引擎校验) 语法层面基本保证 需引擎支持,重试成本
约束解码(grammar / schema) 每步按语法掩码,结构必合法 略降吞吐,需维护语法
# 约束解码:结构由语法保证,参数只需防止语义跑偏
resp = client.chat.completions.create(
    model="MODEL",
    messages=msgs,
    temperature=0.2,
    response_format={"type": "json_schema",
                     "json_schema": {"name": "answer", "schema": SCHEMA}},
    max_tokens=512,
)

分层结论:格式靠约束解码,内容靠低温与提示词。温度调到 0.05 也挡不住漏一个右括号。


8. 任务基线与调整顺序

任务 temperature top_p 备注
事实抽取 / 字段抽取 0–0.2 1.0 或 0.9 优先约束解码
分类判定 0 1.0 配合 logprobs 看置信度
代码生成 0.2–0.4 0.95 停序列必配,禁重复惩罚
摘要 / 改写 0.3–0.7 0.9–0.95 视"忠实度 vs 流畅度"
创意写作 0.8–1.1 0.95–1.0 温度过高开始跑题
多轮对话 0.6–0.8 0.9 与人格设定一起调

顺序固定四步:定任务 → 定评测集 → 只动一个参数 → 记全部参数。跑同一批样本看位移方向。这条纪律比任何具体数值都值钱。

# 参数网格:一次只变一个轴,结果落文件
for T in 0.0 0.3 0.7 1.0; do
  python eval.py --temperature $T --top_p 0.95 --seed 42 \
    --out runs/t${T}.jsonl --model MODEL
done

9. 可复现性的工程做法

措施 作用 局限
固定 seed 采样随机数可复现 不覆盖浮点非确定性
记录全部参数 事后可回放 需连引擎版本一起记
固定模型版本/摘要 排除权重变更 托管服务可能静默更新
固定并发与 batch 减少归约顺序差异 生产流量下难做到
关闭前缀缓存 消除路径差异 牺牲吞吐
{
  "model": "MODEL@sha256:TAG",
  "engine": "vllm 0.x.y",
  "params": {"temperature": 0.3, "top_p": 0.95, "top_k": 0, "seed": 42,
             "max_tokens": 512, "stop": ["\n\n"], "presence_penalty": 0.0},
  "runtime": {"max_num_seqs": 32, "prefix_caching": false}
}

10. 排错:五个高频坑

现象 根因 处理
换了参数说不出哪个起作用 同时改了多个 一次只动一个,其余固定
输出总在句中断 停止序列命中正文 / max_tokens 截断 先看 finish_reason,再看停止词
提高"准确率"靠调低温度 温度只改采样,不改知识 从提示词、检索、约束解码入手
新模型沿用旧参数后变差 不同模型的分布温度标定不同 重跑基线网格,重新定值
同一请求两次结果不同 浮点/batch/缓存差异 见第 3 节

11. 边界:参数管不到的地方

问题 采样参数能否解决 该做什么
模型不知道这个知识 否 检索增强、微调、换模型
输出格式偶发破损 部分 约束解码
首 token 太慢 否 调度与 prefill 优化
事实错误(幻觉) 否 校验链、引用、拒答策略
输出不像品牌口吻 部分 提示词与 few-shot

12. 自测

  1. temperature 与 top-p 分别改变分布的形状还是范围?
  2. 三者同时设置时,通行的生效顺序是什么?为什么顺序会影响结果?
  3. temperature=0 时输出仍可能不同,列出至少三个非确定性来源。
  4. 代码生成任务为什么通常要关掉重复惩罚?
  5. 输出被截断时,先看哪个字段判断是停止序列还是长度上限导致的?
  6. 要让结构化输出"格式零容错",该用低温还是约束解码?为什么?
S23-X05 · 采样参数:temperature、top-p 与 top-k
https://www.clxhxhhr.top/posts/5910/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。