采样参数:输出为什么会波动
正在读取学习记录…
模型生成回答时,会一步步在候选 token 中选择下一项;采样参数影响这一步怎样选。你可以先把 token 当成模型处理的文字片段。比如三个候选的概率是 0.6、0.3、0.1,不同选择规则会改变留下的候选和抽样结果。
这里先用这组三个固定数字手算一次,再运行 Python 模拟。之后回到工单场景:同一句 Prompt 为什么可能产生不同措辞,以及何时应该减少波动。上一篇少量示例改变模型看到的上下文;采样改变给定候选后的选择方式。结果更稳定也不等于事实更正确。
1. 逐个 token 理解生成选择
先把 token 想成模型处理的“小片段”:它可能是一个字、词的一部分,或标点,不一定等于人眼看到的一个词。假设某一步只有“好的”“可以”“抱歉”三个教学候选,概率分别是 0.6、0.3、0.1。选出一个后,模型会结合前文重新计算下一步,不是一次抽完一整篇。下面把候选写成完整词,只为方便手算。
把它想成有10张票的抽签袋:“好的”有6张,“可以”有3张,“抱歉”有1张。温度 T 调整三者的相对票数;top_p 按票数从多到少留下累计达到阈值的一组候选。两步完成后才抽签。先算清这两步,再看种子和平台开关。
2. 手算一次:温度和 top-p 是连续两道处理
对基准概率 p_i,教学程序使用 p_i^(1/T) 后重新归一化。温度低于 1 时大概率项优势更明显,高于 1 时分布更平;T=1 不变。top_p=0.8 时,按温度调整后的概率从大到小保留最少候选,直到累计概率达到或超过0.8,再对保留项重新归一化。T=1 时仍是原来的0.6、0.3、0.1,前两项累计0.9,因此第三项被截断。0.8控制的是一组选项的累计概率。
低温不等于安全。若最高概率答案本来是错的,低温只会更稳定地给出错误。高温也不等于创造力保证;它仅改变候选抽样比例,还可能增加不合契约输出。真实 API 可能有 top_k、惩罚项、长度限制和不同的实现顺序;不要把这里三候选公式外推成所有平台的内部算法。
截断后,概率要重新加到 1
保留前两项后,把 0.6 和 0.3 分别除以总和 0.9,得到约 0.667 和 0.333。这一步叫归一化,即让参与抽样的概率总和回到1。第三项在这次选择中为0;下一步模型重新计算候选,因此仍可能在其他位置生成“抱歉”。
顺序也影响结果:先调温度会改变分布,随后 top-p 留下的集合可能跟着改变。先把这两个中间结果分开看,再看最后的随机计数。
3. Windows 跟做:固定候选的离线实验
将 sampling_lab.py、test_sampling_lab.py 放进同一个新文件夹。在资源管理器打开文件夹,地址栏输入 powershell,确认文件名后运行:
macOS:先在 Python 篇的学习文件夹中打开终端,执行 source .venv/bin/activate。再输入 cd ,把当前例子的文件夹拖入终端并回车,运行下面的命令。
保存为 sampling_lab.py:
"""给三个候选词设置概率,观察温度和top_p怎样改变抽样结果。"""
import random
from collections import Counter
NAMES = ("好的", "可以", "抱歉")
# 与NAMES逐项对应:三个词的初始概率分别为60%、30%、10%。
BASE = (0.6, 0.3, 0.1)
def normalize(values):
if not values or any(v < 0 for v in values) or sum(values) <= 0:
raise ValueError("invalid_probabilities")
total = sum(values)
return [v / total for v in values]
def temperature(probs, t):
if not isinstance(t, (int, float)) or t <= 0:
raise ValueError("temperature_must_be_positive")
# 先按温度调整每个概率,再缩放到总和为1。
return normalize([p ** (1.0 / t) for p in normalize(probs)])
def nucleus(probs, top_p):
if not isinstance(top_p, (int, float)) or not 0 < top_p <= 1:
raise ValueError("top_p_out_of_range")
probs = normalize(probs)
selected = [0.0] * len(probs)
mass = 0.0
# 从概率最大的词开始选,累计概率达到top_p后停止。
for index in sorted(range(len(probs)), key=lambda i: probs[i], reverse=True):
selected[index] = probs[index]
mass += probs[index]
if mass >= top_p:
break
return normalize(selected)
def counts(probs, n=1000, seed=7):
rng = random.Random(seed)
return Counter(rng.choices(NAMES, weights=normalize(probs), k=n))
if __name__ == "__main__":
for t in (0.5, 1.0, 2.0):
adjusted = temperature(BASE, t)
print(f"T={t}: {[round(p, 4) for p in adjusted]}")
filtered = nucleus(BASE, 0.8)
print(f"top_p=0.8: {[round(p, 4) for p in filtered]}")
print(f"offline_counts={dict(counts(filtered))}")完整代码:test_sampling_lab.py
# 核对采样参数的正常结果和错误分支;断言失败时会显示对应的测试。
import unittest
from sampling_lab import BASE, counts, nucleus, normalize, temperature
class SamplingTests(unittest.TestCase):
def test_normal_and_temperature(self):
self.assertAlmostEqual(sum(temperature(BASE, 0.5)), 1)
self.assertGreater(temperature(BASE, 0.5)[0], BASE[0])
self.assertLess(temperature(BASE, 2)[0], BASE[0])
def test_top_p_includes_crossing_candidate(self):
result = nucleus(BASE, 0.8)
self.assertAlmostEqual(result[0], 2/3)
self.assertAlmostEqual(result[1], 1/3)
self.assertEqual(result[2], 0)
self.assertEqual(nucleus(BASE, 0.6), [1, 0, 0])
def test_seed_repeatable(self):
self.assertEqual(counts(BASE), counts(BASE))
self.assertEqual(sum(counts(BASE).values()), 1000)
def test_bad_probabilities(self):
for values in ([], [0, 0], [0.5, -0.5]):
with self.subTest(values=values), self.assertRaisesRegex(ValueError, "invalid_probabilities"):
normalize(values)
def test_bad_parameters(self):
for t in (0, -1):
with self.subTest(t=t), self.assertRaisesRegex(ValueError, "temperature_must_be_positive"):
temperature(BASE, t)
for p in (0, 1.2):
with self.subTest(p=p), self.assertRaisesRegex(ValueError, "top_p_out_of_range"):
nucleus(BASE, p)
if __name__ == "__main__":
unittest.main()ls
python -B -X utf8 sampling_lab.py
python -B -X utf8 -m unittest -v test_sampling_lab.py程序先计算新概率,再用固定种子抽样 1000 次。打印的计数只是这份概率夹具的一次伪随机模拟,不是模型测试或性能基准。源码中的 normalize 保证总和为 1;temperature 要求正数;nucleus 先排序、累计、保留越过阈值者,再重归一化。看核心片段:
def temperature(probs, t):
if t <= 0:
raise ValueError("temperature_must_be_positive")
return normalize([p ** (1.0 / t) for p in probs])主程序底部已经用 for t in (0.5, 1.0, 2.0) 遍历三种温度,无需先改它。先读前三行:约为 [0.7826, 0.1957, 0.0217]、[0.6, 0.3, 0.1]、[0.4727, 0.3343, 0.193]。函数参数 t 就是公式里的温度 T。下一行已用 top_p=0.8,第三项为0。错误输入可直接运行:
python -c "from sampling_lab import temperature,BASE; print(temperature(BASE,0))"
python -c "from sampling_lab import nucleus,BASE; print(nucleus(BASE,1.2))"分别应报 temperature_must_be_positive 和 top_p_out_of_range。正常、负概率与边界检查也在测试里;模块未找到时确认两个文件在同一目录。
先写下预测,再运行对照:t=0.5 时 0.6 那一项应占得更大;t=2 时三个候选应更接近;top_p=0.8 时第三项应被排除。若程序输出与预测不同,逐步打印“温度调整后的概率”“截断前累计值”“截断后的重新归一化值”,定位自己错把哪一步当成了最终结果。1000 次抽样的计数会围绕理论分布波动,不应恰好等于 667、333;固定随机种子只方便重放这一次软件实验。
现在只改一个条件:把 top_p 从 0.8 降到 0.5,先猜第二项是否还会被保留。无需修改上方的完整源码,在同一个 PowerShell 窗口直接运行:
python -c "from sampling_lab import nucleus,BASE; print([round(x,4) for x in nucleus(BASE,0.5)])"
python -c "from sampling_lab import nucleus,BASE; print([round(x,4) for x in nucleus(BASE,0.8)])"预期依次得到 [1.0, 0.0, 0.0] 和 [0.6667, 0.3333, 0.0]。累计概率到 0.6 时已超过 0.5,因此第一行只保留一项;阈值 0.8 则要加入第二项。若结果不同,检查源码中的 BASE 是否仍为 (0.6, 0.3, 0.1),先核对输入概率和计算步骤,再比较抽样结果。
最后做一个反例:把最高概率的候选文字想象成错误的团队标签。此时低温会增加抽到错误标签的机会。正确做法是回到任务契约和标注数据,看看模型为什么给错答案,再用结构与业务校验阻断错误执行。采样参数调优无法替代这些工作。
| 要比较的量 | 计算入口 | 接模型后的测量 |
|---|---|---|
| 概率总和 | normalize 后相加应为1 | 核对接口是否提供概率与其含义 |
| 截断集合 | nucleus 的非零项 | 按接口参数测真实输出变化 |
| 固定种子计数 | counts 默认种子7 | 另测服务端可复现范围 |
| 任务质量 | 固定数字未包含工单判据 | 同输入比较业务正确率 |
对照两种累计阈值
top_p=0.8 时第二项虽只有 0.3,仍须加入它才能让累计值达到阈值。图中最下行的 0.667 / 0.333 是把保留下来的 0.6 / 0.3 除以它们的和 0.9,重新变成总和为 1 的分布。你可以拿纸先算一遍,再用刚才的 nucleus(BASE, 0.8) 核对;这比盯着 1000 次随机抽样的计数更容易看清机制。
4. 在 Agent 产品里怎样做参数试验
把任务分成“需要稳定提取字段”和“允许多种措辞起草”两类。前者先明确输出契约、校验和人工回退;后者比较措辞多样性与人工偏好。一轮只改一个采样参数,固定 Prompt、模型和输入,才能解释结果变化。
真实平台的开关、种子与记录字段
贪心选择直接取当前步最大概率项;采样按分布抽取。Hugging Face 的 do_sample 控制是否采样,温度与 top-p 的适用取决于模式;平台对温度0的处理应查具体接口,不能代入这里要求正数的公式。随机种子方便同实现重放伪随机序列,跨模型、硬件或云服务的确定性要另测。
每组记录模型标识与日期、Prompt版本、输入、参数、最大输出长度、响应、解析/业务失败、延迟和费用。重复次数预先确定,保留全部输出。生成配置文档和生成策略给出框架参数定义;这些记录在真实请求时填写。
| 症状 | 检查证据 | 可能处理 |
|---|---|---|
| 同问不同答 | 模型版本、prompt、采样设置、原始响应 | 固定变量后再复测 |
| JSON 失效 | 原始响应与解析错误类型 | 引入结构化输出,而非只降温 |
| 回答稳定但错误 | 人工标注与来源事实 | 修任务定义或检索,不靠温度 |
| 输出突然截断 | 结束原因、长度上限 | 核查 token 限制,不误判为采样问题 |
成本或时延增加可能来自更多候选、更长回复或重试,而不是温度数值本身直接“收费”。供应商参数范围会变化,上线前应核对当前 API 文档。下篇 结构化输出 会说明为什么概率操纵不能替代运行时校验。
自查与继续学习
温度改变候选的相对概率;top-p 按累计概率截取候选。把练习中的 top-p 从 0.8 改成 0.5,保留集合会从两项变为一项。输出更稳定,不意味着事实更准确,也不能替代权限检查。
自查:能手算 0.6/0.3/0.1 在 top_p=0.8 时保留哪些候选;能跑正常、错误与边界测试;能说明本机模拟与真实模型实验的区别;能列出公平对照时要固定的变量。
课后练习展开习题收起习题
正在读取本地记录…
不调用模型,不执行代码。简答、代码和场景题仅保存答案并展示自查标准,不生成评分、徽章或认证。输入内容不会自动保存,请点击提交;不要填写个人信息或密钥。
选择题 · 先判断边界
关于“采样参数与输出波动”,哪种做法更稳妥?
对应知识点:L1-sampling
简答题 · 解释失败
写出一个失败输入并说明记录字段。
对应知识点:L1-sampling
代码题 · 边界测试
补充正常、边界、失败三组输入和预期结果。
对应知识点:L1-sampling
场景题 · 结果未知
证据不足时如何设计状态和人工接管?
对应知识点:L1-sampling
进一步阅读:Hugging Face 生成配置介绍参数定义;生成策略介绍贪心与采样的区别。
描述会发送给本站维护者,用于修订文章,保存90天。请只填写文章问题,省略个人信息、密钥和内部资料。