采样与解码策略

模型每一步都给出一整个词的概率分布;怎么从中选词,决定了生成文本是呆板还是灵动。

知识课 · 约 30 分钟 · 更新于 2026-08-14

每一步,模型给的其实是一整个分布

自回归生成的每一步,模型输出的并不是「一个词」,而是词表上 所有词 各自的一个概率,也就是一个完整的概率分布。之前我们一直用 贪心解码(greedy decoding) :每步只取概率最大的那个词。

做分类这类有「标准答案」的任务,贪心没问题。但用在开放式 生成 上,它的毛病就暴露了:

有一类任务要的是 整句 最优,比如机器翻译,目标是找出概率最高的那个译文。一句话的概率,就是生成它时每一步条件概率的连乘。贪心每步只顾眼前最大,连乘起来未必最大:这一步选了概率最高的词,可能把后面几步逼进很差的续写,反倒不如先退一步、选个次高的词。为此机器翻译常用 束搜索(beam search) ,每步同时留住多条候选路径往下走,最后比较整句的概率。

用一个三步的例子把这件事摆出来。图里每条边上的数字,是模型在那一步给这个词的条件概率,三步连乘就是整句概率。

第一步「有事」的 0.42 在三个候选里最高,可它后面三个说法 0.38、0.34、0.28 咬得很紧,概率被摊薄。「生病」只有 0.35,低一档,但它后面「请假」一个就占 0.62,接着「了」又占 0.78,三步连乘 ,是贪心这条路的 倍。切到「自己走一遍」,点「生病」往下走,可以自己验一遍。

贪心在第一步就丢掉了「生病」,束搜索把它留到了最后。「有事」后面三个说法的概率咬得很紧(0.38、0.34、0.28),概率被摊薄;「生病」后面「请假」一个就占 0.62,接着「了」又占 0.78。第一步让出 0.07 的概率,换来后两步的集中,整句概率是贪心那条路的 倍。

图中的概率是示例值,词表也只有图里这几个词,用来说明连乘的效果,不是某个真实模型的输出。

但束搜索救不了开放式生成。它找出来的续写整句概率确实更高,读起来却更平淡、更容易兜圈子。人写的句子本来就不总是走概率最高的那条路,一味追求整句概率最大,反而离自然文本更远。

分布里藏着丰富的可能性,问题是怎么用好它。下面换一条路线:采样。

随机采样:按概率抽,而不是只取最大

第一个想法很自然:与其每次取最大,不如 按概率抽样 。概率高的词更可能被抽中,概率低的词也保留一线机会。这样同一个开头,每次生成都可能不同,文本有了 多样性 。

代价出在长尾上。单个尾部词的概率极低,架不住它们数量多。「GPT 与生成式预训练」里那张「我想喝一杯」的分布图,把候选之外剩下的上千个词合并成一根「其余词」柱子,这一根就占了 4%。续写几百步,每一步都按这个概率掷一次骰子,抽中一次几乎是必然。更麻烦的是抽中之后:这个不合适的词进了上文,后面每一步都要基于它往下写,一句话就此跑偏。

「几乎是必然」可以直接算出来。设长尾词合起来占 的概率,一步躲开它们的概率就是 ,连着 步一次都没踩中的概率是 ,于是至少踩中一次的概率为

下图把这条曲线画了出来。横轴是续写的步数 ,纵轴是至少踩中一次长尾词的概率,两个滑块分别控制长尾的总概率 和生成长度。

取 4% 时,17 步之后概率就过半,57 步到九成,续写几百步已经贴着 1。把 压到 1%,曲线只是往右挪,过半需要 69 步、到九成需要 230 步,长度一长照样逼近必然。计算假设每一步的长尾总概率相同、各步独立;真实生成里每一步的分布都在变,这里只用来估量级。

接下来的三种做法都是在纯随机采样的基础上改进,但改的东西不一样: 温度 改变分布本身的形状, Top-k 与 Top-p 则直接限定哪些词可以进入候选。

温度:缩放 logits,改变分布的陡峭程度

回忆 softmax 把一组打分(logits) 变成概率。 温度(temperature) 的做法,是在 softmax 之前先给每个 logit 除以 :

这个除法真正动到的是 logit 之间的 差距 。把两个词的概率相除,分母的求和项约掉,只剩

任意两个词的概率之比,只取决于它们的 分差 ,与 logit 的绝对大小无关。 把每一对分差同比缩成 :分差被放大,概率之比就拉得更开;分差被压缩,概率就互相靠拢。

  • :分差放大,分布更 尖锐 ,高概率的词更突出,输出更确定;
  • :分差不变,就是模型原本的分布,也就是上一节的纯随机采样;
  • :分差压缩,分布更 平缓 ,各词概率被拉平,输出更随机;
  • :极端尖锐,退化为 贪心 (只剩最大那个)。

是一个正实数,不是落在 0 到 1 之间的比例。实践中多在 0.7 到 1.2 这个区间里取值,再往上通常就开始不通顺了。

低温时,输出集中在模型最看好的少数词上,稳定,但也更容易重复;高温时,排名靠后的词更容易被选中,文本更多样,同时更容易出现不通顺、跑题的内容。要留意的是温度只改变分布的形状, 不排除任何词 ,长尾里那些不合适的词在高温下反而更容易被抽中。

下面这组柱子是「我想喝一杯」这一步的「下一个词」分布,词表只有图里这 11 个词。拖动温度滑块,每个 logit 先除以 再过一次 softmax,柱高随之变化;虚线是 时的原始分布,留在图上供对照。

柱高是该词被抽中的概率。把 调到 1 以下,最高的柱子被推得更高、其余压低,滑到最小值 0.05 时概率几乎全落在「水」上,也就是贪心。 大于 1 时各柱互相靠拢。11 根柱子在任何温度下都还在,没有一个词的概率被压成 0,「长尾里不合适的词」那两根在高温下反而更高, 时它们合计 4.6%,是 时 2.0% 的 2.3 倍。图中概率是示例值,不是某个真实模型的输出。

Top-k:只在前 k 个里抽

另一个思路是直接 砍掉长尾 :每步只保留概率最高的 个词,把概率重新归一化后, 只在这 个里采样 。 那些排在后面、不靠谱的词被一刀切掉,既保留了多样性,又挡住了离谱的选择。

实现上不必真的去删词、再手工归一化。把选中之外的词的 logit 置为负无穷,softmax 之后它们的概率就是 0,剩下 个词的概率自动填满整个 1,归一化这一步由 softmax 顺带完成。

实践中 常取几十,比如 40。词表动辄几万,取 40 意味着每一步都把绝大部分长尾直接排除在外。

它的局限是 固定,而每一步的分布形状并不一样。模型很确定时,真正合理的候选可能只有两三个,第 40 名早已明显不合适,却仍然留在候选里;模型很犹豫时,几十个词的概率彼此接近,排在第 41 名的词一样合理,却被一刀切掉。同一个 ,在这两种情形下一个偏松、一个偏紧。

Top-p(核采样):按累计概率自适应地圈定候选

Top-p 采样 ,又叫 核采样(nucleus sampling) 。它不固定候选的个数,而是固定候选的 累计概率 :把词按概率从高到低累加,取累计概率 刚好超过阈值 的那一小撮词(这撮词叫「核」),再在其中采样。

于是候选数量随分布的形状 自适应 变化:

  • 分布 尖锐 时(模型很确定),少数几个词就累加过了 ,候选很少;
  • 分布 平缓 时(模型很犹豫),要凑到 得纳入很多词,候选自然变多。

常取 0.9 左右,也就是每一步只在承载了九成概率的那撮词里选。核之外的词同样是把 logit 置为负无穷剔除,核内几个词的概率经 softmax 重新填满 1,与 Top-k 的做法一致。

现代大模型在生成时,常用 「温度 + Top-p」 的组合:先用温度调整分布陡峭程度,再用 Top-p 圈定候选范围。两者并非各管一段互不相干。温度改的是分布形状,而核的大小恰恰取决于形状,所以升温会把分布压平、让核里装进更多词,降温则让核收缩。调参时它们要一起看。

把两种做法放到同一组分布上比。左边是模型很确定的一步,右边是模型很犹豫的一步,两边的词表都只有图里这 10 个词,按概率从高到低排开。三个滑块分别控制温度 、 和 ,柱子的颜色标出这个词被谁圈进了候选。

柱高是温度缩放后的概率,也是圈定候选的依据;真正采样时核外的词概率归零、核内的词重新归一化到 1(见下一节代码)。折线是从左往右的累计概率,虚线是阈值 ,累计折线第一次越过它的那个词就是核的边界。默认设置( 、 、 )下,确定的那一步里 Top-k 圈进 5 个词,「上海」「天津」「南京」「广州」这四个错误答案全在候选里,Top-p 只留下「北京」「上海」两个;犹豫的那一步反过来,Top-p 要 9 个词才凑够 0.9,Top-k 卡在 5 个,第 6 名的「看」与第 5 名的「找」只差一个百分点,却被挡在外面。把 提到 9,犹豫的那一步刚好够用,确定的那一步却连「中国」「杭州」「香港」也放了进来。温度这一栏是联动的:调到 1.5,确定的那一步核里从 2 个词涨到 6 个;调到 0.7,核收缩到只剩「北京」。图中概率是示例值,不是某个真实模型的输出。

动手:把每种策略画出来

三种策略改的都是 softmax 之前的 logits,采样前的那次 softmax 统一把它们的结果变成分布。Top-p 稍微特殊,它内部要先把 logits 从高到低排序,再过一次 softmax 得到概率、累加起来,才能圈出核,但这次算出的概率只用来定核,最终改动仍然写回 logits。先把三种策略实现出来:

为了把每一步的数看清楚,例子里的词表只有 6 个词, 取 2。真实词表几万个词, 常取 40、 常取 0.9,被压成负无穷的是成千上万个长尾词,留在候选里的仍然只有几十个。

动手:同一个开头,不同策略

沿用「GPT 与生成式预训练」里那个迷你 GPT 的架构,换一份大些的语料重新训练一个,固定同一个开头,分别用贪心、低温、高温、Top-p 生成续写,并排看:

体会得到的规律:温度越高越发散、越低越保守;纯贪心容易重复打转;Top-p 则在合理范围内保持变化。

下面四列用同一个模型、同一个开头,只有采样参数不同,每个字都在你的浏览器里现算。模型很小,四列写出来的句子都不成话,不必比较哪一列更像诗。要看的是每个字下面那根柱子,它标出被抽中的那个字在模型原始分布里排第几名,这才是三种策略在这一步真正改变的东西。

续写由一个字符级 decoder-only 模型现算,结构和上面那段代码一致(3 层、24 维、2 个头,约 5.5 万参数),语料是 3401 首唐代五言诗,共 13.5 万字、1597 个不同的字。四列的名次都按同一把尺子量,也就是模型在这一步给出的原始分布( 、未截断)。贪心的柱子全部贴底,每步都取第 1 名,重新生成多少次都是同一段字;它从第 6 句起绕进一个四句的循环,第 10、11 句原样重复第 6、7 句,第 1 节埋下的重复打转就是这样。低温把柱子压得很矮,将近一半的字仍是第 1 名,名次中位数只有 2。高温和 Top-p 都常常抽到几十名开外的字,两者的分别落在最后一档,高温平均每次有八九个字来自核外,Top-p 那一栏恒为 0。

低温那一栏通常也显示 0,十次生成里大约九次如此,但这两个 0 的来历不同。温度只把长尾的概率压得更低,一个字也没删,多重新生成几次仍可能撞上核外的字;Top-p 把核外的字置成了负无穷,抽多少次都不会出现。另外,多数步里核( )装着上百个字,中位数两百出头,而词表一共 1597 个,所以排在几十名的字仍然稳稳留在核内。

小结:解码策略不改模型,只改「怎么用分布」

关键一点:温度、Top-k、Top-p 都 不改变模型的任何权重 ,只改变「拿到分布后怎么选词」。所以 同一个模型 ,配不同的采样参数,输出可以从高度确定一直调到高度多样。至此,解码器路线的「训练 + 生成」已经完整走通。

能跑了,但用的还是「老配方」

我们手上这条解码器已经能训练、能生成,可它内部用的还是早期 Transformer 的老零件: LayerNorm、加在词向量上的绝对位置编码、普通 FFN 。

第一站是归一化与激活,也就是「RMSNorm 与 SwiGLU」。