图 1:本文封面,温度改形状、截断圈范围的两个面板。
这个系列的前一篇真题解析讲过「大模型为什么会变复读机」,成因机制那边已经讲透,本篇不重复展开。这里换个视角,写一份解码参数的调节手册:模型每生成一个 token 都要从概率分布里做一次选择,贪心、束搜索、temperature、top-k、top-p 这些旋钮各自改变什么、不同任务怎么配。文中的分布示例为演示构造,只做定性说明。
先分两条路线:确定性解码和采样解码。
确定性路线不掷骰子:贪心每步直接取概率最大的 token;束搜索(beam search)每步保留累计分数最高的 num_beams 条候选序列,最后挑整体最优的那条,num_beams=1 时退化为贪心。这条路线稳定、可复现,代价是容易掉进高概率的循环套路,长文本尤其容易复读。采样路线掷骰子:按分布随机抽,Hugging Face 的 generate 里用 do_sample=True 开启,num_beams=1 加 do_sample=True 就是多项式采样。关键认知:temperature、top-k、top-p 这三个参数只对采样路线生效——不开采样,调它们没有任何效果,这是最常见的误用点。
temperature:改分布的形状,不动候选名单。
温度的全部改动只有一处:softmax(z_i / T),logit 先除以 T 再过 softmax。T 小于 1 把 logit 差距放大,分布变尖,头部 token 更垄断;T 大于 1 把差距抹平,尾部 token 也有机会;T 趋近 0 时分布收缩成一个点,效果近似贪心;默认 T=1 保持原分布。用一组构造的 logit 实测(Node.js 22.15.0,node decode.js),词表六个 token「的/是/我/在/一/不」,logit 为 [4.0, 2.5, 2.0, 1.0, 0.5, -1.0]:
function softmax(logits, T = 1) {
const m = Math.max(...logits);
const e = logits.map(z => Math.exp((z - m) / T));
const s = e.reduce((a, b) => a + b, 0);
return e.map(v => v / s);
}
function topK(probs, k) { // 只留前 k 个,其余置零再归一化
const idx = probs.map((p, i) => [p, i]).sort((a, b) => b[0] - a[0]).slice(0, k);
const s = idx.reduce((a, [p]) => a + p, 0);
return idx.map(([p, i]) => [TOKENS[i], p / s]);
}
function topP(probs, p0) { // 按概率累加,凑够 p0 就截断
const sorted = probs.map((p, i) => [p, i]).sort((a, b) => b[0] - a[0]);
const kept = []; let acc = 0;
for (const [p, i] of sorted) { kept.push([p, i]); acc += p; if (acc >= p0) break; }
const s = kept.reduce((a, [p]) => a + p, 0);
return kept.map(([p, i]) => [TOKENS[i], p / s]);
}
实测输出:
T=0.2: P(的)=99.9%, P(是)=0.1%, P(不)=0.0%
T=1: P(的)=69.2%, P(是)=15.4%, P(不)=0.5%
T=2: P(的)=43.1%, P(是)=20.4%, P(不)=3.5%
top-k=3 候选集: 的 73.6%, 是 16.4%, 我 10.0%
top-p=0.9 尖分布(T=1): 留 3 个候选 — 的, 是, 我
top-p=0.9 平分布(T=3): 留 5 个候选 — 的, 是, 我, 在, 一
贪心: 永远是「的」
采样 T=0.2 连采 8 次: 的 / 的 / 的 / 的 / 的 / 的 / 的 / 的
采样 T=2.0 连采 8 次: 的 / 是 / 的 / 是 / 我 / 不 / 是 / 我
T=0.2 时第一名拿走 99.9%,基本等于贪心;T=2 时尾部的「不」从 0.5% 涨到 3.5%——低概率垃圾 token 混进来的风险同步放大。这就是不能只靠拉高温度追求多样性的原因:温度没有判断力,它把好的和差的候选一起抬升。
top-k 与 top-p:温度管形状,截断管范围。
top-k 的做法最直接:只保留概率最高的 k 个 token,其余概率置零再归一化,k=1 时退化为贪心。它的短板是不看分布形状——分布很尖时,第 2 到第 k 名可能都是垃圾照样留下;分布很平时,第 k+1 名可能也很合理却被一刀切掉。top-p(核采样)补上这个缺口:按概率从高到低累加,凑够 p 就截断,候选数随分布形状自适应。上面的实测看得很清楚:同样 top-p=0.9,尖分布只留 3 个候选,平分布自动放到 5 个;top-k=3 则永远是 3 个。Hugging Face 的默认值也能看出定位:top_k 默认 50、top_p 默认 1.0、temperature 默认 1.0——框架默认几乎什么都不拦,截断是你主动给采样路线上的保险,拦的就是被温度抬起来的长尾垃圾。
不同任务怎么配。
创作类(文案、故事、头脑风暴)要发散:T 0.7~1.0 配 top-p 0.9 是常见起点,多样性交给采样,截断负责挡长尾。问答、摘要、翻译要稳:T 0~0.3 或直接贪心;怕回答死板可以给一点温度,但 top-p 别放到 0.9 以上。代码生成要准:贪心或 T 不超过 0.2,少采样——代码里一个偏门 token 就是编译错误,发散没有收益。还有两个纪律:top-k 和 top-p 一般只开一个,叠加使用出了问题说不清是谁在起作用;束搜索属于确定性路线,适合「有标准答案」的输入到输出映射任务,开放式文本里靠加大 num_beams 治不了单调,反而可能更复读。
图 2:两条解码路线、三个参数的分工与任务配置表。
收个尾。 被问「生成结果太死或太飘怎么办」,答题链条是:先确认在不在采样路线(do_sample 开了没),再定性问题——太死就升温或放宽 top-p,太飘就降温或收紧 top-p,复读先查重复惩罚类参数和训练数据,而不是一味升温。你自己调生成参数时踩过最久的坑是哪个?温度的手感,还是截断的候选范围?欢迎聊聊。