← 返回
AI技术

500次采样实测:温度参数如何改写LLM的输出分布

✍️ zhirenhun 📅 2026/10/9 👁 291 阅读 ⏱ 13 分钟
500次采样实测:温度参数如何改写LLM的输出分布

500次采样实测:温度参数如何改写LLM的输出分布

今天刷 dev.to 热榜,看到一篇《LLMs Pass the Data-Science Quiz, Then Give Different Advice: A Kaggle Benchmark of 36 Meas》,讲的是大模型能通过数据科学测验,但每次给出的建议都不一样。评论区一片哀嚎,有人说“AI 没有稳定人格”,有人建议把随机种子焊死。我看完不太服气——所谓的“每次答案不一样”,本质就是温度参数在 softmax 里缩放 logits 造成的采样分布变化,这事儿理论上我背得滚瓜烂熟,但真让我说出“温度从 0.1 拉到 2.0,头部词的概率会跌多少”,我还真答不出具体数字。

理论归理论,没跑过就没有发言权。我决定亲手做一个最小实验:不用任何真实模型,就构造一组固定的词表 logits,手写一个带温度参数的 softmax,然后每个温度采样 500 次,看头部词的实际占比和去重后的不同词数到底怎么变。如果实测频率能逼近理论概率,那“LLM 每次给不同建议”这个现象就完全可以用温度来量化解释,而不是玄学。

热榜上的说法

那篇文章的核心抱怨是:同一份数据科学题目,模型这次这么说,下次换个说法,甚至结论都有细微出入。评论区最有代表性的意见是“我把 temperature 调到 0 就稳定了,但输出变得特别死板”。这句话其实同时点出了两个极端:温度趋近 0 时输出高度确定但缺乏多样性,温度高了又变得难以复现。这两端之间到底发生了什么,很少有人拿数据讲清楚。

我的疑问很具体:温度变化时,概率分布是均匀被“压平”的吗?头部词的概率损失是不是线性的?低温下真的只有极少数词有出场机会吗?这些问题靠读文档和背公式只能得到定性答案,我想要的是一张能看数字的表。所以我干脆自己写一个,500 次采样,五个温度档,一次跑完。

实验设计与代码

实验的骨架很小:我给 8 个词手工指定 logits,"the" 最高 2.1,"a" 次之 1.8,一路往下到 "grass" 的 -1.0。softmax 函数接收一个温度参数,把每个 logits 减去最大值后再除以温度,做指数运算归一化。除以最大值这一步是数值稳定性的常规操作,不影响分布形状。采样子函数每次抽 500 个词,统计出现最多的那个词的占比、实际出场过的不同词数,以及头部词在理论分布里的概率。

温度档我选了 0.1、0.5、0.7、1.0、2.0 五档,其中 0.7 是很多推理框架的默认值,1.0 是“原汁原味”的分布,0.1 和 2.0 则代表两个极端。随机种子固定为 42,保证任何人重跑这份代码得到的都是同样的数字。代码全文如下:

import math, random

random.seed(42)
# Tiny toy "LLM": fixed vocabulary logits + temperature-controlled softmax
logits = {"the": 2.1, "a": 1.8, "code": 1.2, "bug": 0.9,
          "ship": 0.5, "test": 0.2, "walk": -0.3, "grass": -1.0}
words = list(logits)

def softmax(temp):
    mx = max(logits.values())
    exps = {w: math.exp((v - mx) / temp) for w, v in logits.items()}
    s = sum(exps.values())
    return {w: e / s for w, e in exps.items()}

def run(temp, n=500):
    dist = softmax(temp)
    picks = random.choices(words, weights=[dist[w] for w in words], k=n)
    top = max(picks, key=picks.count)
    return top, picks.count(top) / n, len(set(picks)), dist[top]

print(f"{'temp':>5} {'top-word':>9} {'measured':>9} {'theory':>8} {'distinct':>9}")
results = {}
for temp in (0.1, 0.5, 0.7, 1.0, 2.0):
    top, pct, uniq, theo = run(temp)
    results[temp] = (top, pct, uniq)
    print(f"{temp:>5} {top:>9} {pct:>8.0%} {theo:>8.1%} {uniq:>9}")
print()
lo, hi = results[0.1], results[2.0]
print(f"temp 0.1 vs 2.0: top-word share {lo[1]:.0%} -> {hi[1]:.0%}, "
      f"distinct words {lo[2]} -> {hi[2]}")

整个脚本不到三十行,没有依赖任何机器学习库,标准库就够了。这不是因为它简陋,而是因为温度参数的作用机制本来就发生在 logits 到概率这一步,跟模型有多少层、多少参数无关。把它剥出来单独看,反而更干净。

真实运行结果

跑完之后的完整输出是这样的:

实验真实运行输出

数字很说明问题。温度 0.1 时,"the" 占了 500 次采样里的 95%,理论概率是 95.2%,实测几乎完美贴合理论值——但不同词只有 2 个,也就是说 8 个词的词表里有 6 个词在 500 次采样里一次都没被抽中过。温度拉到 2.0 后,"the" 的份额从 95% 直接跌到 21%,不同词数从 2 涨到 8,词表里的每个词都出场了。脚本最后一行汇总得很直白:temp 0.1 vs 2.0,top-word share 95% 到 21%,distinct words 2 到 8。

中间档的过渡也值得看:0.5 时头部占比 49%(理论 53.2%),0.7 时 44%(理论 43.1%),1.0 时 33%(理论 34.1%)。从 0.5 到 1.0,头部词的概率几乎是被腰斩的;从 1.0 到 2.0 又从 33% 掉到 21%。这个衰减明显不是线性的——低温端每挪一点温度,概率都剧烈重排,高温端反而趋于平缓。所谓“调低温度让输出更稳”,在这个实验里被量化成了实打实的百分比差距。

反直觉发现/局限

最反直觉的一点是低温端的坍缩速度。我从直觉上以为温度从 1.0 降到 0.5,头部概率大概从三分之一涨到一半就差不多了,实际是 33% 到 49%,还合理;但继续降到 0.1,直接跳到 95%——也就是说分布不是被慢慢“收窄”,而是突然“坍塌”。这解释了为什么很多人把 temperature 从 0.7 调到 0.3 时感觉输出“突然变得千篇一律”:变化不是渐进的,中间存在一个急剧坍缩的区间。

第二个意外是实测频率对理论概率的贴合程度。五个温度档里最大的偏差出现在 0.5 档:实测 49% 对理论 53.2%,差了 4 个百分点左右;0.7 和 1.0 档的偏差都在 1 到 2 个百分点以内。500 次采样就能到这个精度,说明伯努利方差在这个规模下已经足够小。至于局限,我得老实承认几条:第一,这是 8 个词的玩具词表,真实 LLM 的词表上万,头部坍缩效应在长尾上会更夸张,但也可能出现我这里看不到的多峰结构;第二,我只采样了 500 次,低温档下那些理论概率 1% 以下的词,即使 5000 次也未必采得到,distinct words 的 8 可能只是采样的下限;第三,固定种子 42 意味着这是“一次特定随机流”的结果,换个种子数字会小幅波动,趋势不变但百分比不会逐位复现。

我的结论

这轮实验之后,我对热榜那篇文章讨论的“模型每次建议不一样”有了更具体的看法:那不是模型的毛病,而是温度参数在数学上必然产生的结果。温度 1.0 时头部词只占 33%,剩下 67% 的采样概率分给了其他词——换句话说,就算模型对答案非常有把握,默认温度下也有三分之二的概率“走岔路”。你看到的“不稳定”,是被 softmax 缩放过的概率分布在起作用,每一步 token 生成都是一次掷骰子。

如果你也在被“AI 建议每次都不一样”困扰,我给一个具体可执行的建议:别一刀切调到 0。从这次的数据看,0.7 到 0.5 这个区间里头部概率从 44% 涨到 49%,多样性还保住了 8 个不同词全部出场,是“稳定”和“不呆板”之间性价比最高的位置。真正该警惕的是 0.5 以下——再往下压,你得到的是 95% 单词复读机式的坍缩。我的下一步实验是把这个玩具词表换成一个 5000 词的真实模型词表,看看长尾在高温下会不会把 distinct 数顶到天花板,到时候再来写一篇。

——

广告 广告
🧑‍💻

zhirenhun

一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。

Python LLM 概率统计