Softmax函數與交叉熵

Softmax函數

背景與定義

在Logistic regression二分類問題中,我們可以使用sigmoid函數將輸入Wx + b映射到(0, 1)區間中,從而得到屬於某個類別的概率。將這個問題進行泛化,推廣到多分類問題中,我們可以使用softmax函數,對輸出的值歸一化為概率值。

這裡假設在進入softmax函數之前,已經有模型輸出C值,其中C是要預測的類別數,模型可以是全連接網路的輸出a,其輸出個數為C,即輸出為a_{1}, a_{2}, ..., a_{C}

所以對每個樣本,它屬於類別i的概率為:

y_{i} = frac{e^{a_i}}{sum_{k=1}^{C}e^{a_k}}    forall i in 1...C

通過上式可以保證sum_{i=1}^{C}y_i = 1,即屬於各個類別的概率和為1。

導數

對softmax函數進行求導,即求

frac{partial{y_{i}}}{partial{a_{j}}}

i項的輸出對第j項輸入的偏導。

代入softmax函數表達式,可以得到:

frac{partial{y_{i}}}{partial{a_{j}}} = frac{partial{ frac{e^{a_i}}{sum_{k=1}^{C}e^{a_k}} }}{partial{a_{j}}}

用我們高中就知道的求導規則:對於

f(x) = frac{g(x)}{h(x)}

它的導數為

f(x) = frac{g(x)h(x) - g(x)h(x)}{[h(x)]^2}

所以在我們這個例子中,

g(x) = e^{a_i} \ h(x) = sum_{k=1}^{C}e^{a_k}

上面兩個式子只是代表直接進行替換,而非真的等式。

e^{a_i}(即g(x))對a_j進行求導,要分情況討論:

  1. 如果i = j,則求導結果為e^{a_i}
  2. 如果i 
e j,則求導結果為0

再來看sum_{k=1}^{C}e^{a_k}a_j求導,結果為e^{a_j}

所以,當i = j時:

frac{partial{y_{i}}}{partial{a_{j}}} = frac{partial{ frac{e^{a_i}}{sum_{k=1}^{C}e^{a_k}} }}{partial{a_{j}}}= frac{ e^{a_i}Sigma - e^{a_i}e^{a_j}}{Sigma^2}=frac{e^{a_i}}{Sigma}frac{Sigma - e^{a_j}}{Sigma}=y_i(1 - y_j)

i 
e j時:

frac{partial{y_{i}}}{partial{a_{j}}} = frac{partial{ frac{e^{a_i}}{sum_{k=1}^{C}e^{a_k}} }}{partial{a_{j}}}= frac{ 0 - e^{a_i}e^{a_j}}{Sigma^2}=-frac{e^{a_i}}{Sigma}frac{e^{a_j}}{Sigma}=-y_iy_j

其中,為了方便,令Sigma = sum_{k=1}^{C}e^{a_k}

對softmax函數的求導,我在兩年前微信校招面試基礎研究崗位一面的時候,就遇到過,這個屬於比較基礎的問題。

softmax的計算與數值穩定性

在Python中,softmax函數為:

def softmax(x): exp_x = np.exp(x) return exp_x / np.sum(exp_x)

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

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

但如果輸入值較大時:

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

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

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

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

y_{i} = frac{e^{a_i}}{sum_{k=1}^{C}e^{a_k}}= frac{Ee^{a_i}}{sum_{k=1}^{C}Ee^{a_k}}= frac{e^{a_i+log(E)}}{sum_{k=1}^{C}e^{a_k+log(E)}}= frac{e^{a_i+F}}{sum_{k=1}^{C}e^{a_k+F}}

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

F = -max(a_1, a_2, ..., a_C)

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

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

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

UPDATE(2017-07-07):

有同學問這種近似會不會影響計算結果,為了看原來的softmax函數計算結果怎麼樣,嘗試計算`softmax([1000, 2000, 3000, 4000, 5000])`的值。由於numpy是會溢出的,所以使用Python中的bigfloat庫。

import bigfloatdef softmax_bf(x): exp_x = [bigfloat.exp(y) for y in x] sum_x = sum(exp_x) return [y / sum_x for y in exp_x]res = softmax_bf([1000, 2000, 3000, 4000, 5000])print([%s] % , .join([str(x) for x in res]))

結果:

[6.6385371046556741e-1738, 1.3078390189212505e-1303, 2.5765358729611501e-869, 5.0759588975494576e-435, 1.0000000000000000]

可以看出,雖然前四項結果的量級不一樣,但都是無限接近於0,所以加了一個常數的softmax對原來的結果影響很小。

Loss function

對數似然函數

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

p(t|x) = (y)^t(1-y)^{1-t}

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

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

p(t|x) = prod_{i=1}^{C}P(t_i|x)^{t_i} = prod_{i=1}^{C}y_i^{t_i}

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

-log p(t|x) = -log prod_{i=1}^{C}y_i^{t_i} = -sum_{i = i}^{C} t_{i} log(y_{i})

交叉熵

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

egin{split}D_{KL}(p||q) &= sum_{x in X} p(x) log frac{p(x)}{q(x)} \& =sum_{x in X}p(x)log  p(x) - sum_{x in X}p(x)log  q(x) \& =-H(p) - sum_{x in X}p(x)log q(x)end{split}

這裡H(p)p的熵。

假設有兩個分布pq,它們在給定樣本集上的交叉熵定義為:

CE(p, q) = -sum_{x in X}p(x)log q(x) = H(p) + D_{KL}(p||q)

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

回到我們多分類的問題上,真實的類標籤可以看作是分布,對某個樣本屬於哪個類別可以用One-hot的編碼方式,是一個維度為C的向量,比如在5個類別的分類中,[0, 1, 0, 0, 0]表示該樣本屬於第二個類,其概率值為1。我們把真實的類標籤分布記為p,該分布中,t_i = 1i屬於它的真實類別c。同時,分類模型經過softmax函數之後,也是一個概率分布,因為sum_{i = 1}^{C}{y_i} = 1,所以我們把模型的輸出的分布記為q,它也是一個維度為C的向量,如[0.1, 0.8, 0.05, 0.05, 0]。

對一個樣本來說,真實類標籤分布與模型預測的類標籤分布可以用交叉熵來表示:

l_{CE} = -sum_{i = 1}^{C}t_i log(y_i)

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

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

L = -sum_{k = 1}^{n}sum_{i = 1}^{C}t_{ki} log(y_{ki})

其中t_{ki}是樣本k屬於類別i的概率,y_{ki}是模型對樣本k預測為屬於類別i的概率。

Loss function求導

對單個樣本來說,loss functionl_{CE}對輸入a_j的導數為:

frac{partial l_{CE}}{partial a_j} = -sum_{i = 1}^{C}frac {partial t_i log(y_i)}{partial{a_j}} = -sum_{i = 1}^{C}t_i frac {partial log(y_i)}{partial{a_j}} = -sum_{i = 1}^{C}t_i frac{1}{y_i}frac{partial y_i}{partial a_j}

上面對frac{partial{y_{i}}}{partial{a_{j}}}求導結果已經算出:

i = j時:frac{partial{y_{i}}}{partial{a_{j}}} = y_i(1 - y_j)

i 
e j時:frac{partial{y_{i}}}{partial{a_{j}}} = -y_iy_j

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

egin{split}-sum_{i = 1}^{C}t_i frac{1}{y_i}frac{partial y_i}{partial a_j}&= -frac{t_i}{y_i}frac{partial y_i}{partial a_i} - sum_{i 
e j}^{C} frac{t_i}{y_i}frac{partial y_i}{partial a_j} \& = -frac{t_j}{y_i}y_i(1 - y_j) - sum_{i 
e j}^{C} frac{t_i}{y_i}(-y_iy_j) \& = -t_j + t_jy_j + sum_{i 
e j}^{C}t_iy_j = -t_j + sum_{i = 1}^{C}t_iy_j \& = -t_j + y_jsum_{i = 1}^{C}t_i = y_j - t_jend{split}

TensorFlow

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

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

import tensorflow as tfimport input_datax = 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_fc1w_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))

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

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

import tensorflow as tfimport input_datax = 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_fc1w_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_fc2cross_entropy = -tf.reduce_sum(tf.nn.softmax_cross_entropy_with_logits(labels=label, logits=y))

TensorFlow已經實現好函數,用來計算label和logits的softmax交叉熵。注意,該函數的參數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函數,因為它會幫你處理數值不穩定的問題。

總結

全文到此就要結束了,可以看到,前面介紹這麼多概念,其實只是為了解釋在具體實現時候要做什麼樣的選擇。可能會覺得有些小題大做,但對於NN這個黑盒子來說,我們現暫不能從理論上證明其有效性,那在工程實現上,我們不能再將它當作黑盒子來使用。

Reference

The Softmax function and its derivative Peter』s Notes CS231n Convolutional Neural Networks for Visual Recognition cs229.stanford.edu/note 交叉熵(Cross-Entropy) - rtygbwwwerr的專欄 - 博客頻道 - CSDN.NET difference between tensorflow tf.nn.softmax and tf.nn.softmax_cross_entropy_with_logits

文章同時發在CSDN上:blog.csdn.net/behamcheu#

推薦閱讀:

做出「狼人殺」的 AI 有哪些難點?
深度學習中:」多層的好處是可以用較少的參數表示複雜的函數「這句話該怎麼理解?
如何看待Jeff Dean&Hinton投到ICLR17的MoE的工作?
NIPS 2016有什麼值得關注的呢?
如何評價FAIR的最新工作Data Distillation?

TAG:深度学习DeepLearning | TensorFlow | 机器学习 |