本文收錄在無痛的機器學習第一季。

寫在前面,這篇文章的原創性比較差,因為裡面聊的已經是老生長談的事情,但是為了保持對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

果然不出所料,我們的程式又一次溢位了。

那怎麼辦呢?這裡節省點筆墨,直接照搬老司機的推導過程:(侵刪,我就自己推一遍了……)

CNN--兩個Loss層計算的數值問題

於是,程式碼變成了:

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要小心溢位!

廣告時間

更多精彩盡在《深度學習輕鬆學:核心演算法與視覺實踐》!