数值稳定的softmax

39
有没有一种数值稳定的方法来计算下面的softmax函数? 我在神经网络代码中得到了一些变成NAN的值。
np.exp(x)/np.sum(np.exp(y))

3
这里的答案展示了计算softmax的更好方法:https://dev59.com/vlsW5IYBdhLWcg3wbm4O - Alex Riley
3
在这个链接中,被接受的答案实际上是糟糕的建议。Abhishek,尽管最初似乎不理解为什么,但OP所做的事情是正确的。在softmax中没有数值上困难的步骤,除非出现溢出。因此,尽管在数学上等价,将所有输入向左移动可以消除溢出可能性,因此在数值上是一种改进。 - Paul Panzer
是的,尽管那个被接受的答案的作者在评论中承认减去最大值并不会引入“必要项”,而实际上可以提高数值稳定性(也许该回答应该被编辑...)。无论如何,数值稳定性的问题在其他几个答案中都有所涉及。@AbhishekBhatia:您认为链接是否令您满意地回答了您的问题,或者这里需要一个新的答案? - Alex Riley
4个回答

78

softmax exp(x)/sum(exp(x)) 实际上在数值上表现很好。它只有正项,因此我们不必担心精度丢失,分母至少与分子一样大,因此结果保证落在0和1之间。

唯一可能发生的意外是指数爆炸或者下溢。单个元素的上溢或所有元素的下溢将使输出几乎无用。

但是可以通过使用恒等式 softmax(x) = softmax(x + c) 来防止这种情况,其中c是任意标量:从x中减去max(x)得到一个只有非正元素的向量,消除了上溢,至少有一个元素为零,消除了分母无穷小的情况(某些元素的下溢是无害的,但不是所有元素)。

注:理论上,总和中的灾难性事故是可能的,但您需要一个荒谬的数量级。例如,即使使用只能解决3个小数位的16位浮点数——与“正常”的64位浮点数的15个小数位相比——我们需要2 ^ 1431(〜6 x 10 ^ 431)和2 ^ 1432之间才能得到一个总和,该总和 误差为两倍


47

Softmax函数容易出现两个问题:溢出下溢

溢出:当非常大的数字被近似为无穷大时会发生

下溢:当非常小的数字(接近数轴上的零)被近似(即四舍五入)为时会发生

在进行softmax计算时,为了解决这些问题,一种常见的技巧是通过从所有元素中减去最大值来使输入向量发生平移。对于输入向量x,定义z如下:

z = x-max(x)

然后对新的(稳定的)向量z进行softmax处理


例子:

def stable_softmax(x):
    z = x - max(x)
    numerator = np.exp(z)
    denominator = np.sum(numerator)
    softmax = numerator/denominator

    return softmax

# input vector
In [267]: vec = np.array([1, 2, 3, 4, 5])
In [268]: stable_softmax(vec)
Out[268]: array([ 0.01165623,  0.03168492,  0.08612854,  0.23412166,  0.63640865])

# input vector with really large number, prone to overflow issue
In [269]: vec = np.array([12345, 67890, 99999999])
In [270]: stable_softmax(vec)
Out[270]: array([ 0.,  0.,  1.])

在上述情况中,我们通过使用stable_softmax()成功避免了溢出问题。

更多细节请参见深度学习书中的数值计算章节。


我不确定通过减去最大值来处理下溢是否是最好的方法。但正如Paul的答案所建议的那样,下溢不是很重要的问题。 - Wiza

9
扩展 @kmario23 的答案,以支持一维或二维numpy数组或列表。 如果您通过softmax传递结果的批处理,则2D张量(假设第一维是批处理维度)很常见:
import numpy as np


def stable_softmax(x):
    z = x - np.max(x, axis=-1, keepdims=True)
    numerator = np.exp(z)
    denominator = np.sum(numerator, axis=-1, keepdims=True)
    softmax = numerator / denominator
    return softmax


test1 = np.array([12345, 67890, 99999999])  # 1D numpy
test2 = np.array([[12345, 67890, 99999999], # 2D numpy
                  [123, 678, 88888888]])    #
test3 = [12345, 67890, 999999999]           # 1D list
test4 = [[12345, 67890, 999999999]]         # 2D list

print(stable_softmax(test1))
print(stable_softmax(test2))
print(stable_softmax(test3))
print(stable_softmax(test4))

 [0. 0. 1.]

[[0. 0. 1.]
 [0. 0. 1.]]

 [0. 0. 1.]

[[0. 0. 1.]]

1
这对我来说仍然是下溢。 - Nihar Karve
我已经使用这个工具相当长的时间了,从未出现过问题。你确定输入中没有NaN或Inf吗? - David Parks
1
我现在明白了 - np.seterr(all='raise') 将会抱怨大值的下溢,即使函数是正确的。这确实是最好的解决方案。 - Nihar Karve
1
这是一份黄金答案,运行得非常好。请务必注意这个解决方案,因为我尝试了超过半打所谓的softmax解决方案,而这个是唯一一个真正起作用的。 - Allohvk

-5

在你的情况下计算softmax函数没有问题。问题似乎来自于梯度爆炸或者是训练方法中出现的这种问题。要么通过"剪裁值",要么"选择正确的权重初始分布"来解决这些问题。


9
在您的情况下,计算softmax函数没有问题。尝试使用它计算softmax(800) - Warren Weckesser
在那个规模下做任何事情都会导致“inf”,如果你试图在Python中处理那个规模,那么任何东西都是不稳定的。 - amir hossein hajavi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接