1. 程式人生 > >softmax交叉熵損失函式求導

softmax交叉熵損失函式求導

softmax 函式

softmax(柔性最大值)函式,一般在神經網路中, softmax可以作為分類任務的輸出層。其實可以認為softmax輸出的是幾個類別選擇的概率,比如我有一個分類任務,要分為三個類,softmax函式可以根據它們相對的大小,輸出三個類別選取的概率,並且概率和為1。

softmax函式的公式是這種形式:

Si=ezi∑kezkSi=ezi∑kezk

SiSi代表的是第i個神經元的輸出。 ok,其實就是在輸出後面套一個這個函式,在推導之前,我們統一一下網路中的各個表示符號,避免後面突然出現一個什麼符號懵逼推導不下去了。 首先是神經元的輸出,一個神經元如下圖: 神經元的輸出設為:

zi=∑jwijxij+bzi=∑jwijxij+b

其中wijwij是第ii個神經元的第jj個權重,bb是偏移值。zizi表示該網路的第ii個輸出。 給這個輸出加上一個softmax函式,那就變成了這樣:

ai=ezi∑kezkai=ezi∑kezk

aiai代表softmax的第i個輸出值,右側就是套用了softmax函式。

損失函式 loss function

在神經網路反向傳播中,要求一個損失函式,這個損失函式其實表示的是真實值與網路的估計值的誤差,知道誤差了,才能知道怎樣去修改網路中的權重。

損失函式可以有很多形式,這裡用的是交叉熵函式,主要是由於這個求導結果比較簡單,易於計算,並且交叉熵解決某些損失函式學習緩慢的問題。交叉熵的函式是這樣的:

C=−∑iyilnaiC=−∑iyiln⁡ai

其中yiyi表示真實的分類結果。 到這裡可能嵌套了好幾層,不過不要擔心,下面會一步步推導,強烈推薦在紙上寫一寫,有時候光看看著看著就迷糊了,自己邊看邊推導更有利於理解~

最後的準備

在我最開始看softmax推導的時候,有時候看到一半不知道是怎麼推出來的,其實主要是因為一些求導法則忘記了,唉~ 所以這裡把基礎的求導法則和公式貼出來~有些忘記的朋友可以先大概看一下:

推導過程

好了,這下正式開始~ 首先,我們要明確一下我們要求什麼,我們要求的是我們的loss對於神經元輸出(zizi)的梯度,即:

∂C∂zi∂C∂zi

根據複合函式求導法則:

∂C∂zi=∂C∂aj∂aj∂zi∂C∂zi=∂C∂aj∂aj∂zi

有個人可能有疑問了,這裡為什麼是ajaj而不是aiai,這裡要看一下softmax的公式了,因為softmax公式的特性,它的分母包含了所有神經元的輸出,所以,對於不等於i的其他輸出裡面,也包含著zizi,所有的aa都要納入到計算範圍中,並且後面的計算可以看到需要分為i=ji=j和i≠ji≠j兩種情況求導。 下面我們一個一個推:

∂C∂aj=∂(−∑jyjlnaj)∂aj=−∑jyj1aj∂C∂aj=∂(−∑jyjln⁡aj)∂aj=−∑jyj1aj

第二個稍微複雜一點,我們先把它分為兩種情況: ①如果i=ji=j:

∂ai∂zi=∂(ezi∑kezk)∂zi=∑kezkezi−(ezi)2(∑kezk)2=(ezi∑kezk)(1−ezi∑kezk)=ai(1−ai)∂ai∂zi=∂(ezi∑kezk)∂zi=∑kezkezi−(ezi)2(∑kezk)2=(ezi∑kezk)(1−ezi∑kezk)=ai(1−ai)

②如果i≠ji≠j:

∂aj∂zi=∂(ezj∑kezk)∂zi=−ezj(1∑kezk)2ezi=−aiaj∂aj∂zi=∂(ezj∑kezk)∂zi=−ezj(1∑kezk)2ezi=−aiaj

ok,接下來我們只需要把上面的組合起來:

∂C∂zi=(−∑jyj1aj)∂aj∂zi=−yiaiai(1−ai)+∑j≠iyjajaiaj=−yi+yiai+∑j≠iyjai=−yi+ai∑jyj∂C∂zi=(−∑jyj1aj)∂aj∂zi=−yiaiai(1−ai)+∑j≠iyjajaiaj=−yi+yiai+∑j≠iyjai=−yi+ai∑jyj

最後的結果看起來簡單了很多,最後,針對分類問題,我們給定的結果yiyi最終只會有一個類別是1,其他類別都是0,因此,對於分類問題,這個梯度等於:

∂C∂zi=ai−yi

softmax的計算與數值穩定性

在Python中,softmax函式為:

def softmax(x):
    exp_x = np.exp(x)
    return exp_x / np.sum(exp_x)
  • 1
  • 2
  • 3

傳入[1, 2, 3, 4, 5]的向量

>>> softmax([1, 2, 3, 4, 5])
array([ 0.01165623,  0.03168492,  0.08612854,  0.23412166,  0.63640865])
  • 1
  • 2

但如果輸入值較大時:

>>> softmax([1000, 2000, 3000, 4000, 5000])
array([ nan,  nan,  nan,  nan,  nan])
  • 1
  • 2

這是因為在求exp(x)時候溢位了:

import math
math.exp(1000)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# OverflowError: math range error
  • 1
  • 2
  • 3
  • 4
  • 5

一種簡單有效避免該問題的方法就是讓exp(x)中的x值不要那麼大或那麼小,在softmax函式的分式上下分別乘以一個非零常數:

yi=eai∑Ck=1eak=Eeai∑Ck=1Eeak=eai+log(E)∑Ck=1eak+log(E)=eai+F∑Ck=1eak+Fyi=eai∑k=1Ceak=Eeai∑k=1CEeak=eai+log(E)∑k=1Ceak+log(E)=eai+F∑k=1Ceak+F

這裡log(E)log(E)是個常數,所以可以令它等於FF。加上常數FF之後,等式與原來還是相等的,所以我們可以考慮怎麼選取常數FF。我們的想法是讓所有的輸入在0附近,這樣eaieai的值不會太大,所以可以讓FF的值為:

F=−max(a1,a2,...,aC)F=−max(a1,a2,...,aC)

這樣子將所有的輸入平移到0附近(當然需要假設所有輸入之間的數值上較為接近),同時,除了最大值,其他輸入值都被平移成負數,ee為底的指數函式,越小越接近0,這種方式比得到nan的結果更好。

def softmax(x):
    shift_x = x - np.max(x)
    exp_x = np.exp(shift_x)
    return exp_x / np.sum(exp_x)
  • 1
  • 2
  • 3
  • 4
>>> softmax([1000, 2000, 3000, 4000, 5000])
array([ 0.,  0.,  0.,  0.,  1.])
  • 1
  • 2

當然這種做法也不是最完美的,因為softmax函式不可能產生0值,但這總比出現nan的結果好,並且真實的結果也是非常接近0的。

Loss function

對數似然函式

機器學習裡面,對模型的訓練都是對Loss function進行優化,在分類問題中,我們一般使用最大似然估計(Maximum likelihood estimation)來構造損失函式。對於輸入的xx,其對應的類標籤為tt,我們的目標是找到這樣的θθ使得p(t|x)p(t|x)最大。在二分類的問題中,我們有:

p(t|x)=(y)t(1−y)1−tp(t|x)=(y)t(1−y)1−t

其中,y=f(x)y=f(x)是模型預測的概率值,tt是樣本對應的類標籤。

將問題泛化為更一般的情況,多分類問題:

p(t|x)=∏i=1CP(ti|x)ti=∏i=1Cytiip(t|x)=∏i=1CP(ti|x)ti=∏i=1Cyiti

由於連乘可能導致最終結果接近0的問題,一般對似然函式取對數的負數,變成最小化對數似然函式。

−log p(t|x)=−log∏i=1Cytii=−∑i=iCtilog(yi)−log p(t|x)=−log∏i=1Cyiti=−∑i=iCtilog(yi)

交叉熵

說交叉熵之前先介紹相對熵,相對熵又稱為KL散度(Kullback-Leibler Divergence),用來衡量兩個分佈之間的距離,記為DKL(p||q)DKL(p||q)

DKL(p||q)=∑x∈Xp(x)logp(x)q(x)=∑x∈Xp(x)log p(x)−∑x∈Xp(x)log q(x)=−H(p)−∑x∈Xp(x)log q(x)DKL(p||q)=∑x∈Xp(x)logp(x)q(x)=∑x∈Xp(x)log p(x)−∑x∈Xp(x)log q(x)=−H(p)−∑x∈Xp(x)log q(x)

這裡H(p)H(p)是pp的熵。

假設有兩個分佈pp和qq,它們在給定樣本集上的相對熵定義為:

CE(p,q)=−∑x∈Xp(x)log q(x)=H(p)+DKL(p||q)CE(p,q)=−∑x∈Xp(x)log q(x)=H(p)+DKL(p||q)

從這裡可以看出,交叉熵和相對熵相差了H(p)H(p),而當pp已知的時候,H(p)H(p)是個常數,所以交叉熵和相對熵在這裡是等價的,反映了分佈pp和qq之間的相似程度。關於熵與交叉熵等概念,可以參考該部落格再做了解。

回到我們多分類的問題上,真實的類標籤可以看作是分佈,對某個樣本屬於哪個類別可以用One-hot的編碼方式,是一個維度為CC的向量,比如在5個類別的分類中,[0, 1, 0, 0, 0]表示該樣本屬於第二個類,其概率值為1。我們把真實的類標籤分佈記為pp,該分佈中,ti=1ti=1當ii屬於它的真實類別cc。同時,分類模型經過softmax函式之後,也是一個概率分佈,因為∑Ci=1yi=1∑i=1Cyi=1,所以我們把模型的輸出的分佈記為qq,它也是一個維度為CC的向量,如[0.1, 0.8, 0.05, 0.05, 0]。 對一個樣本來說,真實類標籤分佈與模型預測的類標籤分佈可以用交叉熵來表示:

lCE=−∑i=1Ctilog(yi)lCE=−∑i=1Ctilog(yi)

可以看出,該等式於上面對數似然函式的形式一樣!

最終,對所有的樣本,我們有以下loss function:

L=−∑k=1n∑i=1Ctkilog(yki)L=−∑k=1n∑i=1Ctkilog(yki)

其中tkitki是樣本kk屬於類別ii的概率,ykiyki是模型對樣本kk預測為屬於類別ii的概率。

Loss function求導

對單個樣本來說,loss function lCElCE對輸入ajaj的導數為:

∂lCE∂aj=−∑i=1C∂tilog(yi)∂aj=−∑i=1Cti∂log(yi)∂aj=−∑i=1Cti1yi∂yi∂aj∂lCE∂aj=−∑i=1C∂tilog(yi)∂aj=−∑i=1Cti∂log(yi)∂aj=−∑i=1Cti1yi∂yi∂aj

上面對∂yi∂aj∂yi∂aj求導結果已經算出:

當i=ji=j時:∂yi∂aj=yi(1−yj)∂yi∂aj=yi(1−yj)

當i≠ji≠j時:∂yi∂aj=−yiyj∂yi∂aj=−yiyj

所以,將求導結果代入上式:

−∑i=1Cti1yi∂yi∂aj=−tiyi∂yi∂ai−∑i≠jCtiyi∂yi∂aj=−tjyiyi(1−yj)−∑i≠jCtiyi(−yiyj)=−tj+tjyj+∑i≠jCtiyj=−tj+∑i=1Ctiyj=−tj+yj∑i=1Cti=yj−tj−∑i=1Cti1yi∂yi∂aj=−tiyi∂yi∂ai−∑i≠jCtiyi∂yi∂aj=−tjyiyi(1−yj)−∑i≠jCtiyi(−yiyj)=−tj+tjyj+∑i≠jCtiyj=−tj+∑i=1Ctiyj=−tj+yj∑i=1Cti=yj−tj

TensorFlow

方法1:手動實現(不建議使用)

在TensorFlow中,已經有實現好softmax函式,所以我們可以自己構造交叉熵損失函式:

import tensorflow as tf
import input_data

x = tf.placeholder("float", shape=[None, 784])
label = tf.placeholder("float", shape=[None, 10])

w_fc1 = tf.Variable(tf.truncated_normal([784, 1024], stddev=0.1))
b_fc1 = tf.Variable(tf.constant(0.1, shape=[1024]))
h_fc1 = tf.matmul(x, w_fc1) + b_fc1

w_fc2 = tf.Variable(tf.truncated_normal([1024, 10], stddev=0.1))
b_fc2 = tf.Variable(tf.constant(0.1, shape=[10]))
y = tf.nn.softmax(tf.matmul(h_fc1, w_fc2) + b_fc2)

cross_entropy = -tf.reduce_sum(label * tf.log(y))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

cross_entropy = -tf.reduce_sum(label * tf.log(y))是交叉熵的實現。先對所有的輸出用softmax進行轉換為概率值,再套用交叉熵的公式。

方法2:使用tf.nn.softmax_cross_entropy_with_logits(推薦使用)

import tensorflow as tf
import input_data

x = tf.placeholder("float", shape=[None, 784])
label = tf.placeholder("float", shape=[None, 10])

w_fc1 = tf.Variable(tf.truncated_normal([784, 1024], stddev=0.1))
b_fc1 = tf.Variable(tf.constant(0.1, shape=[1024]))
h_fc1 = tf.matmul(x, w_fc1) + b_fc1

w_fc2 = tf.Variable(tf.truncated_normal([1024, 10], stddev=0.1))
b_fc2 = tf.Variable(tf.constant(0.1, shape=[10]))
y = tf.matmul(h_fc1, w_fc2) + b_fc2

cross_entropy = -tf.reduce_sum(tf.nn.softmax_cross_entropy_with_logits(labels=label, logits=y))
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

TensorFlow已經實現好函式,用來計算labellogitssoftmax交叉熵。注意,該函式的引數logits在函式內會用softmax進行處理,所以傳進來時不能是softmax的輸出了。

區別

既然我們可以自己實現交叉熵的損失函式,為什麼TensorFlow還要再實現tf.nn.softmax_cross_entropy_with_logits函式呢?

這個問題在Stack overflow上已經有Google的人出來回答(傳送門),原話是:

If you want to do optimization to minimize the cross entropy, AND you’re softmaxing after your last layer, you should use tf.nn.softmax_cross_entropy_with_logits instead of doing it yourself, because it covers numerically unstable corner cases in the mathematically right way. Otherwise, you’ll end up hacking it by adding little epsilons here and there.

也就是說,方法1自己實現的方法會有在前文說的數值不穩定的問題,需要自己在softmax函式裡面加些trick。所以官方推薦如果使用的loss function是最小化交叉熵,並且,最後一層是要經過softmax函式處理,則最好使用tf.nn.softmax_cross_entropy_with_logits函式,因為它會幫你處理數值不穩定的問題。