Softmax:为什么要减最大值

黎 浩然/ 13 10 月, 2026/ 大语言模型/LARGELANGUAGEMODEL/LLM, 机器学习/MACHINELEARNING, 研究生/POSTGRADUATE, 计算机/COMPUTER/ 0 comments

Softmax 直接计算 exp(x) 可能溢出。先减去这一行的最大 logit,可以保留数学上的概率,同时把指数输入压到不大于零。要计算对数概率时,再进一步使用 log-softmax,避免先得到零再取对数。

文章目录
  1. 公式没错,计算仍可能失败
  2. 减最大值为什么不改变概率
  3. 低概率更适合留在对数域
  4. 可运行的最小实现
  5. 注意力掩码还多一道边界
  6. 参考资料

公式没错,计算仍可能失败

分类器与注意力都常用 pᵢ=exp(xᵢ)/Σⱼexp(xⱼ)。其中 x 是一行有限实数,求和范围必须是同一组候选。这个公式说明相对权重,却没有保证浮点指数函数能表示中间结果。

拿人为构造的 [1000,1001,1002] 来看,三个数相差不大,概率应当平滑变化;直接取指数却可能超出浮点表示范围。问题发生在归一化之前,所以最后“再除一个很大的数”来不及挽救中间值。

原始 logits
1000 · 1001 · 1002
共同平移
-2 · -1 · 0
归一化概率
0.090031 · 0.244728 · 0.665241
原创示意图,数值为文中人工构造例子的计算结果,不是模型实测。

减最大值为什么不改变概率

设 m=max(x),分子分母同时乘 exp(-m),得到 pᵢ=exp(xᵢ-m)/Σⱼexp(xⱼ-m)。共同因子消掉了。对有限输入,xᵢ-m≤0,最大项的指数为1,至少有一个正的可表示权重。

因此上面的三个 logit 可以先改成 [-2,-1,0]。概率约为 [0.090031,0.244728,0.665241]。这是教学计算,不是某个模型的真实预测。共同平移不改结果;把数值整体乘一个系数则会改变相对差距,不能混为一谈。

Blanchard 与两位 Higham 的数值分析论文讨论了这些等价公式的浮点表现。这里采用的是已长期使用的平移形式,不是2026年的新方法;本文也不复现论文的全部误差分析。

低概率更适合留在对数域

平移解决了大正指数的溢出风险,但很小的权重仍可能下溢为零。例如 [0,-1000] 的第二个概率,在本例 Python 浮点计算中会变成0。数学上它仍大于0。

如果随后计算 log(0),就丢失了本来有限的对数概率。直接计算 log pᵢ=(xᵢ-m)-log Σⱼexp(xⱼ-m),能绕开这次“先转概率”的信息损失。注意仍有浮点舍入,它不是任意范围、任意精度都完全准确的承诺。

它也解释了困惑度计算为什么更适合从模型对数概率出发,而不是反复在概率和对数间转换。

可运行的最小实现

import math

def softmax(xs):
    if not xs or not all(math.isfinite(x) for x in xs):
        raise ValueError("Expected nonempty finite logits")
    m = max(xs)
    weights = [math.exp(x - m) for x in xs]
    total = math.fsum(weights)
    return [w / total for w in weights]

def log_softmax(xs):
    if not xs or not all(math.isfinite(x) for x in xs):
        raise ValueError("Expected nonempty finite logits")
    m = max(xs)
    shifted = [x - m for x in xs]
    log_total = math.log(math.fsum(math.exp(x) for x in shifted))
    return [x - log_total for x in shifted]

p = softmax([1000.0, 1001.0, 1002.0])
assert math.isclose(math.fsum(p), 1.0)
assert all(math.isclose(a, b) for a, b in zip(p, softmax([0., 1., 2.])))
assert softmax([7.0]) == [1.0]
assert math.isfinite(log_softmax([0., -1000.])[1])
assert softmax([0., -1000.])[1] == 0.0
for bad in ([], [math.nan], [math.inf]):
    try:
        softmax(bad)
    except ValueError:
        pass
    else:
        raise AssertionError("Invalid input accepted")
print([round(x, 6) for x in p])
print(log_softmax([0., -1000.]))

此原创 Python 示例已执行:检查概率和、平移不变性、单元素输入、低概率下溢与对数域结果,以及空输入和非有限值。输出概率为 [0.090031,0.244728,0.665241],第二组对数概率为 [0,-1000]。

这里没有训练模型,也没有运行 GPU 内核。时间复杂度 O(n),列表占额外 O(n) 空间,n 为候选数;假定每次基础浮点运算按常数成本计。

注意力掩码还多一道边界

生产实现常用负无穷表示被掩码的位置。本例明确只接受有限 logit,不支持这种输入。如果一整行都被掩码,直接套用减最大值会出现负无穷减负无穷,不能靠普通 Softmax 自动定义合理答案。

应在调用链中明确掩码和空行策略,并使用所选框架经过验证的实现。排查 NaN 时先检查输入、归一化维度和全掩码行,再考虑精度;减最大值不会修复上游已经产生的 NaN。与注意力缩放 √d相比,这里的平移保留概率,缩放则改变分布。

Read the English version

参考资料

本文于2026年10月13日补发,文章日期保留原计划北京时间。参考资料早于计划时段,示例为原创教学计算。

支持

如果这篇文章对你有帮助,欢迎支持本站。

微信支持二维码,点击查看大图
微信
支付宝支持二维码,点击查看大图
支付宝
Buy Me a Coffee,点击支持本站
Buy Me a Coffee

二维码可点击放大。更多支持方式见支持页面。

Share this Post

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

*
*