本文收錄在無痛的機器學習第一季。
寫在前面,這篇文章的原創性比較差,因為裡面聊的已經是老生長談的事情,但是為了保持對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要小心溢位!
廣告時間
更多精彩盡在《深度學習輕鬆學:核心演算法與視覺實踐》!