直接用np.exp(x)算Softmax会出错,因为exp(1000)等大数导致上溢为inf,分母求和后出现nan或0/0;解决方法是先减去每行最大值再计算,即softmax(x)_i = exp(x_i−max(x)) / ∑_j exp(x_j−max(x)),确保最大指数为0,彻底避免上溢。

为什么直接用 np.exp(x) 算 Softmax 会出错
因为 np.exp 对较大输入(比如 100)会返回 inf,导致整个 Softmax 输出全为 nan 或 0/0。常见现象是训练中突然 loss 变成 nan,或前向传播后输出全是 0 和 nan。
根本原因是指数函数增长极快,而 Softmax 的分母是所有 exp(x_i) 之和 —— 只要有一个 x_i 超过约 709(np.finfo(np.float64).maxexp),np.exp(x_i) 就溢出为 inf。
解决思路不是“缩放输入”,而是利用 Softmax 的数学等价性:softmax(x)_i = exp(x_i) / sum_j(exp(x_j)) = exp(x_i - C) / sum_j(exp(x_j - C)),其中 C 是任意常数。选 C = max(x) 后,最大项变成 exp(0) = 1,其余项 ≤ 1,彻底避开上溢。
如何正确实现带防溢出的 Softmax(逐行解释)
关键就两步:先减去每行最大值,再算 exp 和归一化。注意必须按 axis 操作,尤其对 batch 输入(2D):
立即学习“Python免费学习笔记(深入)”;
Python 编程规范与最佳实践。强制 PEP 8 代码风格、语法校验、单元测试执行,适用于代码编写、审查和重构。触发词:Python、代码规范、py_compile、单元测试。
import numpy as np <p>def softmax(x):</p><h1>x: shape (N, D) 或 (D,)</h1><pre class='brush:python;toolbar:false;'>x_max = np.max(x, axis=-1, keepdims=True) # 保持维度,方便广播 x_shifted = x - x_max exp_x = np.exp(x_shifted) return exp_x / np.sum(exp_x, axis=-1, keepdims=True)
-
axis=-1表示对最后一维(通常是类别维)操作,兼容 1D 和 2D 输入 -
keepdims=True必须加,否则x_max形状塌缩,无法和原x广播相减 - 不要写
np.max(x, axis=1)处理 batch —— 若输入是 (5,),这会报错;axis=-1更鲁棒
常见错误写法及后果
这些写法看似简洁,但会在特定输入下失效:
- 漏掉
keepdims=True→x - np.max(x, axis=-1)触发广播错误或静默计算错误(如 (3,4) 减 (3,) 得到意外形状) - 写成
np.exp(x) / np.sum(np.exp(x), axis=-1, keepdims=True)→ 未平移,仍可能溢出 - 对整个数组取 max:
C = np.max(x)→ 错误地把 batch 内所有样本混在一起平移,破坏每个样本的相对概率分布 - 用
np.clip(x, -700, 700)替代平移 → 生硬截断会扭曲梯度,且不能解决分母求和时的中间溢出
验证是否真的防住了溢出
手动构造极端输入测试:
x_bad = np.array([1000.0, 1001.0, 1002.0]) print(softmax(x_bad)) # 输出:[0.09003057 0.24472847 0.66524096] —— 正常,非 nan <p>x_2d = np.array([[1000, 2000], [3000, 4000]]) print(softmax(x_2d))</p><h1>输出两行有效概率,每行和为 1.0
对比不平移版本:np.exp(x_bad) / np.sum(np.exp(x_bad)) 会返回 [nan nan nan]。
真正容易被忽略的是:这个技巧只解决上溢(inf),不解决下溢(极小值变 0.0);但下溢在 Softmax 中影响有限——对应概率本就接近 0,且不会导致除零或 nan。需要梯度稳定时,才需额外考虑 log-Softmax。

















