CNN--两个Loss层计算的数值问题

本文收录在无痛的机器学习第一季

写在前面,这篇文章的原创性比较差,因为里面聊的已经是老生长谈的事情,但是为了保持对CNN问题的完整性,还是把它单独拿出来写一篇了。已经知道的童鞋可以忽略,没看过的童鞋可以来瞧瞧。

这次我们来聊一聊在计算Loss部分是可能出现的一些小问题以及现在的解决方法。其实也是仔细阅读下Caffe代码中有关Softmax loss和sigmoid cross entropy loss两个部分的真实计算方法。

Softmax

有关Softmax的起源以及深层含义这里不多说了,我们直接来看看从定义出发的计算方法:

def naive_softmax(x):
    y = np.exp(x)
    return y / np.sum(y)

随便生成一组数据,计算一下:

a = np.random.rand(10)
print a
print naive_softmax(a)

[ 0.67362493  0.20352691  0.02024274  0.29988184  0.2319521           
  0.43930833  0.98219225  0.54569955  0.00298489  0.83399241]
[ 0.12203807  0.07626659  0.06349434  0.08398094  0.07846559   
  0.09654569  0.16615155  0.10738362  0.06240797  0.14326563]

从结果来看比较正常,符合预期,但是如果我们的输入不那么正常呢?

b = np.random.rand(10) * 1000
print b
print naive_softmax(b)

[ 497.46732916  227.75385779  537.82669096  787.54950048  663.13861524
  224.69389572  958.39441314  139.09633232  381.35034548  604.08586655]
[  0.   0.   0.  nan   0.   0.  nan   0.   0.   0.]

我们发现数值溢出了,因为指数函数是一个很容易让数值爆炸的函数,那么输入大概到多少会溢出呢?蛋疼的我还是做了一个实验:

np.exp(709)
8.2184074615549724e+307

这是在python能够正常输出的单一数字的极限了。实际上这接近double类型的数值极限了。

虽然我们前面讲过有一些方法可以控制住数字,使输出不会那么大,但是终究难免会有个别大数字使得计算溢出。而且实际场景中计算softmax的向量维度可能会比较大,大家累积起来的数字有时还是挺吓人的。

那么如何解决呢?我们只要给每个数字除以一个大数,保证它不溢出,问题不就解决了?老司机给出的方案是找出输入数据中最大的数,然后除以e的最大数次幂,相当于下面的代码:

def high_level_softmax(x):
    max_val = np.max(x)
    x -= max_val
    return naive_softmax(x)

这样一来,之前的问题就解决了,数值不再溢出了。

b = np.random.rand(10) * 1000
print b
print high_level_softmax(b)

[ 903.27437996  260.68316085   22.31677464  544.80611744  506.26848644
  698.38019158  833.72024087  200.55675076  924.07740602  909.39841128]

[  9.23337324e-010   7.79004225e-289   0.00000000e+000   
   1.92562645e-165   3.53094986e-182   9.57072864e-099   
   5.73299537e-040   6.01134555e-315   9.99999577e-001   
   4.21690097e-007]

虽然不溢出了,但是这个结果看着还是有点怪。上面的例子中最大的数字924.07740602的结果高达0.99999,而其他一众数字经过softmax之后都小的可怜,小到我们用肉眼无法从坐标轴上把它们区分出来,这说明softmax的最终结果和scale有很大的关系。

为了让这些小的可怜的数字不那么可怜,使用一点平滑的小技巧还是很有必要的,于是代码又变成:

def practical_softmax(x):
    max_val = np.max(x)
    x -= max_val
    y = np.exp(x)
    y[y < 1e-20] = 1e-20
    return y / np.sum(y)

结果变成了:

[  9.23337325e-10   9.99999577e-21   9.99999577e-21   9.99999577e-21
   9.99999577e-21   9.99999577e-21   9.99999577e-21   9.99999577e-21
   9.99999577e-01   4.21690096e-07]

看上去比上面的还是要好一些,虽然不能扭转一家独大的局面。

Sigmoid Cross Entropy Loss

从上面的例子我们可以看出,exp这个函数实在是有毒。下面又轮到另外一个中毒专业户sigmoid出厂了。这里我们同样不解释算法原理,直接出代码:

def naive_sigmoid_loss(x, t):
    y = 1 / (1 + np.exp(-x))
    return -np.sum(t * np.log(y) + (1 - t) * np.log(1 - y)) / y.shape[0]

我们给出一个温和的例子:

a = np.random.rand(10)
b = a > 0.5
print a
print b
print naive_sigmoid_loss(a,b)

[ 0.39962673  0.04308825  0.18672843  0.05445796  0.82770513  
  0.16295996  0.18544111  0.57409273  0.63078192  0.62763516]
[False False False False  True False False  True  True  True]
0.63712381656

下面自然是一个暴力的例子:

a = np.random.rand(10)* 1000
b = a > 500
print a
print b
print naive_sigmoid_loss(a,b)

[  63.20798359  958.94378279  250.75385942  895.49371345  965.62635077
   81.1217712   423.36466749  532.20604694  333.45425951  185.72621262]
[False  True False  True  True False False  True False False]
nan

果然不出所料,我们的程序又一次溢出了。

那怎么办呢?这里节省点笔墨,直接照搬老司机的推导过程:(侵删,我就自己推一遍了……)


于是,代码变成了:

def high_level_sigmoid_loss(x, t):
    first = (t - (x > 0)) * x
    second = np.log(1 + np.exp(x - 2 * x * (x > 0)))
    return -np.sum(first - second) / x.shape[0]

举一个例子:

a = np.random.rand(10)* 1000 - 500
b = a > 0
print a
print b
print high_level_sigmoid_loss(a,b)

[-173.48716596  462.06216262 -417.78666769    6.10480948  340.13986055
   23.64615392  256.33358957 -332.46689674  416.88593348 -246.51402684]
[False  True False  True  True  True  True False  True False]
0.000222961919658

这样一来数值的问题也就解决了!

就剩一句话了

计算中遇到Exp要小心溢出!

广告时间

更多精彩尽在《深度学习轻松学:核心算法与视觉实践》

编辑于 2017-11-22 12:49