我一开始也想不通。扩散语言模型跑得好好的输出也稳就是偶尔有点“呆”同一个提示词翻来覆去就那几个句式。我想当然地觉得既然温度系数能调节采样的随机性那就把最后选词的 argmax 换成加 temperature 的随机采样呗一行代码的事效果不就来了结果折腾完一跑输出直接放飞自我语法没问题但语义整段崩坏。后来把训练脚本、采样链条、logits 分布全部重新过了一遍才意识到问题远不是“改一行 argmax”这么简单。这个标题看着像是个小需求实际上牵扯到扩散语言模型整个生成范式的核心它压根不是自回归模型那种“逐词预测下一个”的玩法而是从一个充满噪声的序列出发通过多步去噪逐步“雕刻”出完整句子。温度这个参数在自回归模型里只是改改 softmax 的手感在扩散模型里它要介入的是每一轮去噪过程中的中间分布改一个地方等于动了整条流水线的配方。这篇文章我把扩散语言模型的采样机制、temperature 的数学本质、以及为什么不能照搬 argmax 替换这种“自回归惯性思维”讲透配合我实际调参的步骤和踩坑记录给同样在做扩散 LM 生成、文本编辑、或者想给生成结果加点可控随机性的朋友一个可复现的参考。1. 内容整体设计与思路拆解先搞清楚扩散 LM 到底怎么“说话”的1.1 扩散语言模型和自回归模型的生成逻辑根本不是一回事拿 GPT 这类自回归模型来说生成的每一步都是“给定前文预测下一个词的条件概率”。它天然是序列化的从左到右一次蹦一个 token你可以随时介入把最后一步的概率分布改一改比如调温度、做 top-k、top-p影响的就是“下一个词选谁”。扩散语言模型走的是另一条路。它一开始生成的不是第一个词而是一整段充满噪声的序列然后通过一个迭代去噪过程把噪声逐步“擦掉”最后还原成一段完整文本。这个思路和图像扩散模型里的去噪过程很像只不过把连续像素换成了离散 token 的分布。这里有个关键点扩散模型每一步迭代输出的并不是一个具体的 token 序列而是一个“对整段序列中每个位置的概率分布预测”。模型在每个位置都给出一个概率分布表示“这个位置最终更像哪个 token”。去噪过程就是不断修正这些分布让它们从一团糊的均匀分布逐渐收敛到接近 one-hot 的确定分布。所以扩散 LM 的采样不是一个“选词”的动作而是一个“整段逐步成形”的动作。你改任何一个位置的分布影响的都不只是那个位置而是后续所有去噪轮次对这个位置的进一步修正。1.2 为什么第一反应是“改 argmax”这个思路错在哪很多做 NLP 的同行包括我自己第一反应就是把采样时取概率最大 token 的 argmax 操作换成带温度系数的随机采样。在自回归模型里这招确实有效因为每一步的分布是独立的条件概率你缩放它、扰动它只影响当前这个 token 的选择虽然也会误差累积但至少是“生成路径”层面的扰动。但扩散 LM 里的 argmax 并不是“当前步的选择”而是“最终输出的近似估计”。它在整个去噪过程中可能被调用多轮或者只用于最后一步的分布转换。不管哪种argmax 处理的对象都是“已经过多次迭代修正的边缘分布”不是“下一步的条件分布”。直接在最后一步把 argmax 换成 temperature 采样你实际上是在说“我相信这个位置最终的分布是对的但我不想选概率最大的我想随机一点。”这在自回归里是合理的因为模型每一步都在用真实的前文做条件但在扩散 LM 里最后一步的分布是多个去噪步累积出来的它本身已经包含了模型对整句一致性的“妥协”。你在这个节骨眼上随机采样等于把前面所有轮的协调结果最后一下推翻了。1.3 我最终采用的思路温度介入去噪全过程而不是只改输出层调试几轮之后我重新把采样流程拆解发现正确的做法是把温度作为一个“调度参数”引入每一步去噪时的分布缩放而不是最后输出时才干预。这样每个位置的分布从一开始的平坦状态到逐步尖锐化再到最终确定全程都受到温度的控制。具体来说每一步去噪预测出 logits 后都除以一个温度值再进 softmax然后再根据这个缩放后的分布去计算下一步的噪声估计。这样温度影响的不是最终一个“选词”动作而是整个去噪轨迹的形状高温度让每一步的分布更平滑、更犹豫低温度让分布更尖锐、更果断。最后再从收敛的分布里采样得到有随机性又不失控的文本。这条思路后面我会给出具体的代码实现和参数调节过程先记住一个核心温度在扩散 LM 里调节的是“去噪的路径”不只是“选词的手感”。2. 核心细节解析与实操要点temperature 在去噪链条里的真实作用2.1 先厘清温度在概率分布里的数学作用别停留在“随机程度”这种模糊说法temperature 对概率分布的作用本质是改变分布的“熵特性”。给定 logits z带温度 T 的 softmax 公式是p_i exp(z_i / T) / sum_j exp(z_j / T)T 大于 1 时所有概率被“拉平”分布更均匀采样结果更随机T 小于 1 时概率差异被“放大”高概率 token 更突出分布更尖锐T 趋向 0 时分布趋向 one-hot等价于 argmax。这看起来很简单但在扩散 LM 里我们面对的不是一个静态的分布而是一串随时间变化的分布序列。每一步去噪的 logits 分布其形状本身就在演进。早期去噪步分布通常是平坦的因为模型还不确定每个位置到底是什么后期去噪步分布逐渐尖锐模型开始“下注”。所以你在不同阶段用同一个温度效果完全不一样。早期用一个较大温度会让本就不明确的分布更糊去噪信号变弱模型难以收敛后期用较大温度会让快确定的分布出现不该有的扰动导致最终文本出现局部逻辑断裂。2.2 不同温度策略对扩散 LM 的实际影响对比我做了三组对比实验用同一份校验集、同一个预训练扩散 LM只改温度介入方式观察生成文本的指标和人工感受温度策略实现方式文本多样性语义连贯性备注不介入全程 argmax最后取概率最大低句式重复高逻辑稳定基线只改最后一步最后一步 distribution 除以 T 再采样中等局部词汇有变化低句子结构断裂明显我一开始的错误做法全程缩放每一步 logits 都除以 T高随 T 增大而增大中高T0.8~1.2 时较稳推荐做法动态温度调度早期用偏低温度后期逐步升高最高叙事风格多变中短句受影响小长句偶有问题需要额外调调度曲线只改最后一步的做法之所以最差是因为它把温度加在了“错误的时间点”。这时候分布已经经过多轮迭代各位置之间形成了相互约束的联合关系你单独随机化每个位置的采样等于同时引入多维多处随机扰动而且没有任何后续机制来“纠正”这些扰动。在自回归模型里随机选词后下一步模型还能看到你选的词并调整后续生成在扩散 LM 里最后一步之后没有下一步了。这就是本质差异。2.3 实操要点每步缩放 logits 时要注意数值边界和下溢问题全程缩放 logits 实现起来不难但有几个细节必须注意。第一个是 logits 的数值范围。扩散 LM 的输出层 logits 经过多次归一化后通常分布在一个比较窄的区间内直接除以一个很小的温度比如 0.5 以下可能导致部分 logits 指数溢出。我实际测过某些位置的 logits 最大能在 15 左右T0.4 时 exp(15/0.4) 会直接爆掉softmax 变成 NaN。所以必须对 logits 做 clip或者用 log-softmax 的稳定形式。我习惯把 logits 先减去最大值再缩放保证数值稳定性。第二个是温度不能对所有维度一视同仁。扩散 LM 的 token 空间里有些 token 是功能性的比如句号、逗号、空格它们的分布通常很尖锐模型几乎确定而名词、动词等实义词的分布在早期去噪步是相对平滑的。如果整体缩放 logits标点类 token 的位置可能因为扰动而出现不应该的替换导致输出看起来“每个词都对但标点乱了”。这个问题我后面用 position-wise 温度调节缓解了就是在序列维度上按置信度动态调整温度。第三个是随机种子。加了温度采样后同样输入在不同运行下输出不同这本身是预期行为。但调试时如果不固定种子你很难判断效果变化是温度引起还是随机噪声引起。我把随机种子固定在验证实验里作为“对照组”调参确定后再放开。3. 实操过程与核心环节实现从“一行替换”到完整温度调度3.1 实现前先看采样流程的代码长什么样我用的扩散 LM 采样流程大致是下面这个结构先看伪代码逻辑def sample(model, noise, steps, vocab_size): x noise # 初始噪声序列 for t in range(steps, 0, -1): logits model(x, t) # 预测每个位置的 logits # 这里是关键如何从 logits 得到下一步的 x probs softmax(logits) x update_x(x, probs, t) # 去噪一步 return argmax(logits, dim-1) # 最后取概率最大的初看这个结构会觉得最后一行 argmax 换成torch.multinomial(probs / T)就完事了。但注意这里的x在循环里是连续值表示不是离散 token模型每步预测的 logits 是用来“修正噪声”的不是用来“选词”的。你改最后一步的选词方式对前面已经完成的去噪没有任何反馈。正确的介入点是在probs softmax(logits)这行前后对 logits 本身做缩放。每一步缩放后x的更新方向会受到温度影响从而改变整条去噪轨迹。3.2 完整实现带温度调度的扩散 LM 采样我最终实现的是一个带温度调度temperature schedule的采样函数伪代码改成 Python 风格实际使用时可套用你自己的采样框架import torch import torch.nn.functional as F torch.no_grad() def sample_with_temperature_schedule( model, noise_seq, steps64, vocab_size32000, T_start0.6, T_end1.2, clip_range(-10, 10), seed42 ): torch.manual_seed(seed) x noise_seq.clone() for t in range(steps, 0, -1): # 温度按去噪进度线性变化早期更果断后期更随机 progress (steps - t) / steps # 0 - 1 T T_start (T_end - T_start) * progress logits model(x, t) # [batch, seq_len, vocab] logits logits - logits.max(dim-1, keepdimTrue).values logits torch.clamp(logits, *clip_range) scaled_logits logits / T # 温度介入点 probs F.softmax(scaled_logits, dim-1) # 去噪更新不同模型实现不同这里示意 x denoise_step(x, probs, t) # 最后一步用温度采样选词而不是 argmax final_logits model(x, 0) final_logits final_logits - final_logits.max(dim-1, keepdimTrue).values final_logits torch.clamp(final_logits, *clip_range) T_final 0.85 # 固定最终温度 final_probs F.softmax(final_logits / T_final, dim-1) # 按概率采样而非取最大 out_tokens torch.multinomial(final_probs, num_samples1).squeeze(-1) return out_tokens def denoise_step(x, probs, t): # 伪代码根据概率分布修正 x实际需接入模型自身的反向过程 return x - 0.1 * (x - probs_to_embedding(probs))3.3 为什么我选择“早期低温后期高温”的调度这个调度方向可能和直觉相反。有人会想早期模型不确定时应该温度高一点多探索后期确定时温度低一点稳定输出。但实测下来对扩散 LM 完全相反。早期去噪时x里还残留大量噪声模型预测的 logits 分布非常平坦本来就接近均匀分布。如果你再加大温度分布更平等于告诉模型“每个词都差不多”去噪信号模糊后续迭代很难收敛到有意义的结构。这时用偏低温度让模型在所有可能性里“强行选一个相对更可能的倾向”反而能尽早确立句子骨架。后期去噪时分布逐渐尖锐但可能存在“多个合理的候选”互相竞争。这时把温度略微调高让模型不要过早锁死在某个局部最优保留一定的随机性最终输出会更有变化、更像人写的。这个结论在不同数据集上强度略有差异但方向是一致的。如果你只在最后一步加温度是无法体会到这个调度效应的因为前面所有步都已经敲定了结构最后再怎么随机也只换词不换骨。3.4 参数选择的计算过程和实验记录我以 T_start0.6、T_end1.2 为基线逐步扫描参数组合每组固定验证集 500 条提示计算输出文本的 n-gram 重复率和困惑度。结果如下T_startT_end4-gram 重复率困惑度越低越好人工评分1-5越高越好0.60.612.3%18.43.1基本不随机等于基线0.61.28.7%21.24.2多样性提升语义稳定0.61.86.5%27.83.4开始出现逻辑跳跃1.01.29.5%20.13.8不如 0.6 起步0.41.210.2%19.53.6早期温度压得过低反而锁死句式从表里可以看到温度调度不是无脑拉高“探索”。0.6 起步、1.2 收尾是最稳的区间。超过 1.5 后后期分布被拉得太平多义词之间频繁切换整句思想漂移。这里的计算过程不复杂难点在于控制变量——除了温度策略不同其余采样参数、种子、提示词全部一致才能看出真实差异。3.5 第三个隐藏问题位置级置信度差异怎么处理前面提到的标点问题让我加了 position-wise 的温度修正。思路是每步去噪时模型其实会给出一个置信度分数一般看 logits 最大值和次大值的差距。对置信度高的位置温度压低对置信度低的位置温度适当放宽。实现如下def apply_positional_temperature(logits, T_base, alpha0.3): # 计算每个位置的最大 logit 差值作为置信度 top2 torch.topk(logits, k2, dim-1).values margin top2[..., 0] - top2[..., 1] # [batch, seq] # margin 大 - 置信度高 - 温度低 T_pos T_base / (1 alpha * margin.unsqueeze(-1)) scaled_logits logits / T_pos return scaled_logits这个做法的好处是置信度高的位置基本不受温度影响保持不变性置信度低的位置获得更多随机性增加表达变化。我加上之后标点乱换的问题基本消失名词位置也能看到一定的多样性整体输出质量比全局温度方案更稳。4. 常见问题与排查技巧实录照着做基本能绕开的坑4.1 为什么加了温度后输出语法正确但逻辑混乱这是我把温度加在“最后一步”时最典型的症状。语法正确说明局部 token 的分布本身没有坏模型还是知道每个位置该放什么词性的词逻辑混乱说明句子层面的结构约束被打破了。在扩散 LM 里句子逻辑不是由一个隐变量统一控制的而是通过多步去噪逐步“协商”出来的。最后一步随机采样等于在协商完成后又让每个位置独立篡改结果彼此之间没有重新协商逻辑自然崩。排查方法把中间轮次的 logits 分布可视化看每个位置的 argmax 序列在去噪过程中是如何变化的。如果分布收敛得很好只在最后一步被采样打乱那问题基本就出在介入时机上。你应该把温度提前到中间步骤而不是只碰最后一下。4.2 温度调大后输出“百花齐放”但经常跑题跑题和逻辑混乱本质不同。逻辑混乱是句子内部成分不协调跑题是整个句子的主题漂移。后者通常是温度调得过高模型在某个早期去噪步就开始在多个主题候选之间摇摆并且把这种摇摆带进了后续所有去噪步。这种情况我建议先降低 T_end而不是下调整体温度。你保留早期结构的稳定性只放宽后期细节的随机性。如果 T_end 降到 1.0 附近还是有跑题那就要检查是不是位置级温度让某些关键实词位置随机化幅度太大可以加大 alpha 值让高置信度位置更“稳”。4.3 logits 溢出和 NaN怎么稳准狠地定位温度过低时logits 除以 T 会迅速拉大数值常见表现是 loss 变成 NaN 或输出全为 [UNK]。我踩过这个坑一开始以为是模型训练出了问题后来打印中间 logits 才发现某些 token 的原始 logit 就在 10 以上除以 0.3 直接指数爆炸。解决步骤先对 logits 做 max-subtraction再 clamp 到 [-10, 10]最后才做温度缩放。另外建议把温度下限设到 0.5 以上除非你对 logits 分布做了细致的统计。不同模型、不同训练方式的 logits 分布差异很大动手前先跑一批数据看 logits 的分布直方图心里有数再定温度范围。4.4 加了温度后输出质量不稳定时好时坏这种情况大多来自两个原因一是随机种子没有固定每次采样自然会有波动二是没有做温度调度的“退火”处理模型在去噪后期仍然残留较大的随机性导致同样输入、同样温度不同运行结果之间方差很大。我习惯在验证时固定种子在正式应用时只对 T_end 以上的高温度做限制必要时对 final_probs 做 top-p 过滤比如只从前 100 个候选里采样避免极端低概率 token 偶尔冒出来。这样既保留多样性又把方差控制在可接受范围。4.5 常见问题速查表问题原因解决方案语法对逻辑乱温度只加在最后一步采样将温度介入提前到每步去噪的 logits 缩放或低温起步内容跑题后期温度过高主题摇摆降低 T_end或增加位置级温度对高置信度位置的保护NaN / 全 UNKlogits 溢出先减最大值再缩放clamp 到 [-10, 10]温度下限不低于 0.5输出时好时坏随机种子未固定方差大验证时固定种子或加 top-p 过滤限制极端采样标点乱换全局温度干扰了高置信度结构位置按 logits top-2 差值做 position-wise 温度修正5. 扩展思考这个“温度”能不能进一步做成可学习的策略顺着这个思路往下走我后来还试过把温度调度参数本身做成一个小网络输入是当前去噪步、当前噪声水平和各位置置信度输出是每一步的温度值。这个做法有点类似学习一个“采样策略”其实并不复杂就一个 MLP 回归模块用强化学习或者简单的演化搜索来优化最终文本的评分指标。效果有提升但代价是要引入额外的搜索空间和训练成本。如果项目时间充裕这个方向值得尝试如果只是想让生成结果多一点变化又不想牺牲稳定性前面的线性调度配合 position-wise 修正已经足够好用。另外如果你手里的扩散模型是用 continuous-time 框架训练的温度调度的曲线可以做得更平滑比如基于噪声水平的余弦退火调度比线性调度更贴合模型的去噪节奏。这个取决于具体训练框架我没有办法给出一个通用公式建议你直接把自己的 noise schedule 函数拿出来把温度设为该 schedule 的映射效果会比盲调好很多。最后再分享一个我自己的体会扩散语言模型里所有“生成策略”的参数本质都是在“干预去噪轨迹”不是在“干预选词”。你带着这个视角去看 temperature、top-p、repetition penalty它们的实现位置和生效时机自然会浮现出来。别被“改一行 argmax”这种直觉带偏先搞清楚你的模型在哪一步决定“说什么”再决定温度该加在哪。
阅读完成 · 觉得有帮助?