2.6 Softmax編碼實戰
講了太多的理論知識,接下來我們將動手實現第一個學習模型——Softmax[16]多分類器。該分類器是深度學習中網絡輸出層的默認分類器,因此也可以認為是最簡單的多分類“神經網絡”。首先我們會介紹該模型的一些理論知識,然后你需要使用該模型并結合上述我們介紹的機器學習知識,完整地完成CIFAR-10分類圖像識別任務。
假如讓你來設計一個分類互斥的三分類函數你會怎么設計呢?你可能一開始會有點不知所措,但記住,當我們想要創造點什么的時候,我們首先要思考的其實是我們擁有著什么。
可以把三分類任務當作是三個人來處理,特定的人就代表著擅長于判斷特定的類別。那每個人可以做些什么呢?其實很簡單,就如同線性回歸一樣,每個人只是對數據特征進行加權求和。如式(2.21)所示,將數據維度乘以權重再求和,也將其稱為評分函數(score function)。

z就表示每個人心中的分類得分,有三個人,也就會有三個z值,選取其中最高分,也就是“最自信”的分值即可。
那這就有一個問題,比如一個識別小貓的任務,得分可能是(50,40,45),也可能是(25,1,2)。那么請問哪一組評分系統好?雖然兩組中第一列都是最高得分,但明顯后者的相對得分更為突出,那就相當于第二組中的第一列,比其他列有更高的自信心。因此除了關注最高分以外,還要關心其相對比值,也就是將各自得分再除以總分即可。
如式(2.22)所示,就是我們設計的分類公式。非常簡單,我們僅僅將所有人的分數之和作為公分母,然后將每個人各自的分數作為分子,最后我們就得到了總和為1的多分類函數,這一步也叫歸一化概率(Normalized Probabilities)。

我們將式(2.21)與式(2.22)合并一下,得到式(2.23),就是其完整的表達式,雖然看起來比較復雜,但是所表達含義卻非常簡單。

上述表達式還缺點東西,因為我們想要預測得分比盡可能的高,但得分函數是線性的,其增長幅度有限。那可不可以在不影響其單調性的情況下,使其相對得分更顯著呢?如式(2.24)所示,這就是我們修改之后的表達式,由于指數函數是一個單調函數,因此這樣的修改并不會對表達式含義有任何影響。

相應地,我們將這些改變全部寫在一起,就是式(2.25)。

我們將其推廣至k類,如式(2.26)所示,這就是Softmax函數表達式。

第一眼看見該表達式,可能會有點抓狂,但其實這并不復雜。我們之所以花那么多“無用”的時間來推導出該公式,只是想告訴你,只要你不再害怕,不再抗拒時,靜下心來你就會發現這有多簡單。
Softmax函數的輸出是一個k維向量,比如函數輸出為[0.1,0.3,0.7],而該數據的真實標記為第二類,其向量表示就應該是[0,1,0]。那我們如何去刻畫其誤差呢?你也許會想到將各自的誤差加起來,比如使用均方誤差作為代價函數,那么該數據的誤差就應該如下所示。

但實際上我們多算了一些誤差,因為這三個輸出是彼此依賴的,調整其中一個,其余都會受到影響。因此正確的代價誤差應該如式(2.27)所示。

雖然Softmax的輸出為k個輸出,但我們僅對其真正關鍵的那一個輸出進行修改就可以。為了簡化接下來的描述,我們將引入如式(2.28)所示的示例函數。

其使用方法非常簡單,表達式的輸出為真則輸出1,表達式的輸出為假則輸出0,例如:I{2+3=4}=0,I{1+1=2}=1。
Softmax代價函數就可表示為式(2.29),其中y表示數據的真實類標,比如y = 3,就表示該數據為第三類數據,該代價函數所表達的含義其實就為遍歷所有輸出,將真實類標對應的輸出的誤差,作為我們的代價函數。

對我們來說,更為實際的可能是梯度的計算公式,因為使用代價函數推導梯度計算公式后,我們才能根據其修改權重。這里不進行Softmax的梯度推導,直接給出梯度計算公式,如式(2.30)所示。

其中,,若你不太習慣式(2.30)的寫法,可以寫成式(2.31),可能更有助于記憶。

這其實就是交叉熵代價函數在多類任務的推廣,再將式(2.31)換一種寫法就得到式(2.32)。

這就是典型的二分類交叉熵梯度計算公式。
- Softmax參數冗余
Softmax存在著一個問題,那就是參數冗余。假設我們進行二分類任務,Softmax的輸出為一個二維向量,就相當于有著兩套數據參數,在邏輯回歸中,預測生病的概率為0.7,那不生病的概率很自然就是0.3。而使用Softmax的方法使我們計算出生病的概率為0.7,但同樣也計算出了不生病的概率為0.3。因此Softmax神經元總是比實際需要多了一套參數,但這相比于神經網絡的上億參數,簡直微乎其微,這里僅作為冷門知識了解即可。
2.6.1 編碼說明
在本章的練習中我們將要逐步完成以下內容。
1.熟悉使用CIFAR-10數據集;
2.編碼softmax_loss_naive函數,使用顯式循環計算損失函數以及梯度;
3.編碼softmax_loss_vectorized函數,使用向量化表達式計算損失函數及其梯度;
4.編碼隨機梯度下降算法,訓練一個Softmax分類器;
5.使用驗證數據選擇超參數。
完整的教程請參考“第2章練習-實現softmax.ipynb”文件順序練習,該處僅僅對重要內容進行解釋說明。在本章結尾將會提供參考代碼,希望你“走投無路”時再查看這些“錦囊”。請記住“Practice makes perfect”。
首先啟動Jupyter:如圖2-6所示,啟動dos窗口,將路徑轉換到文件:“第2章練習-實現softmax.ipynb”所在路徑,然后輸入:jupyter notebook,再按Enter鍵確認。這時,瀏覽器會自動啟動Jupyer,單擊本章練習即可。

圖2-6 啟動Jupyter
首先,我們需要導入各模塊,由于Python 2.7+系列在默認情況下不支持中文字符,因此,你需要在每個文件的開頭添加一行“#-*- coding: utf-8 -*-”,才能使用中文注釋,否則編譯器將會拋出編碼異常錯誤。本章所需完成的代碼模塊全部存放在文件“DLAction/classifiers/chapter2”中,而諸如數據導入和梯度檢驗等模塊被統一存放在了“DLAction/utils”目錄下,讀者可自行查看。

2.6.2 熟練使用CIFAR-10 數據集
CIFAR-10是一套包含了60000張,大小為32×32的十分類圖片數據集,其中50000張圖片被分為了訓練數據,10000張圖片被分為測試數據,存放在“DLAction/datasets/cifar-10-batches-py”目錄下,也可以通過訪問互聯網地址進行下載http://www.cs.toronto.edu/~kriz/cifar.html。由于我們使用的是彩色圖片,因此每張圖片都會有三個色道。當我們將磁盤圖片導入到內存時,該數據集存放在形狀如下的NumPy數組中。

載入CIFAR-10數據的各項操作已經被封裝進了load_CIFAR10函數中,只需要載入數據的存放目錄,就可以得到劃分好的訓練數據、訓練數據類標、測試數據和測試數據類標。

如果覺得這些數據還是有些抽象,你也可以使用下列的代碼塊,可視化地查看載入CIFAR-10數據集圖片。

其圖像如圖2-7所示。

圖2-7 CIFAR-10數據集
- 數據預處理
數據劃分:原始數據量可能稍大,這樣不利于我們的編碼測試,因此我們會采樣250條訓練數據作為樣本訓練數據X_sample,采樣100條數據作為樣本驗證數據X_validation,作為編碼階段測試使用。
數據形狀轉換:原始的數據集可看成是四維數組(數據個數,寬,高,色道),這樣不太方便使用,因此我們將(寬,高,色道)壓縮在一維上,其數據形式變為(數據個數,數據維度),而數據維度=寬×高×色道。數據形狀轉換代碼如下所示。

數據歸一化(Normalization):在通常情況下,我們需要對輸入數據進行歸一化處理,也就是使得數據呈均值為零,方差為1的標準正態分布。由于圖像的特征范圍在[0,255],其方差已經被約束了,我們只需要將數據進行零均值中心化處理即可,不需要將數據壓縮在[-1,1]范圍(當然,也可以進行此項處理)。因此在處理時,只需要減去其數據均值即可。注意:我們計算的是訓練數據的均值,而不是全部數據的均值,你需要時刻警惕不要“偷看期末試卷”,測試數據是“未知的”。數據歸一化實現代碼如下所示。

添加偏置項:偏置項(bias)也可以看作是線性函數的常數項或截距,在實際中并不特別地區分偏置項參數與權重參數,并且其對于訓練效果的影響也非常有限。但為了遵照傳統,我們還是將其默認地使用在算法中,通過在數據中增加一維值為常數1的特征作為bias對應的輸入特征,然后將其統一到權重參數中,最終的圖片數據維度就為dim=32×32×3+1。其實現的代碼如下所示。

我們將這些步驟統統封裝在get_CIFAR10_data()函數中。運行該函數,將得到以下結果。

2.6.3 顯式循環計算損失函數及其梯度
首先使用循環的方式實現Softmax分類器的損失函數(代價函數),打開“DLAction/classifiers/chapter2/softmax_loss.py”文件并實現softmax_loss_naive函數。

注意:請盡量少地使用循環,使用的循環越少,就可以節約更多的計算時間。需要逐漸地適應向量化計算表達,因為采用向量化表達,既書寫簡潔,使得代碼的可讀性大大提高,不容易產生錯誤,又極大地提高了運算效率。例如計算每類得分的指數,就可以直接使用scores_E=np.exp(X[i].dot(W))函數,更多NumPy的用法請參考第1章1.3 Python簡易教程。

完成了上述代碼后,要怎樣判斷實現是否正確?一個比較直接的方法就是計算其損失值。在不加入權重懲罰的情況下,所實現的Softmax損失值應該接近于-log(0.1)。為什么是0.1?如果是5分類任務,該損失值又該接近于多少呢?損失值驗證代碼塊如下所示。

其正確結果近似于這樣。

接下來我們進行梯度檢驗,精確的數值梯度是使用極限的方式求解梯度,如式(2.33)所示。

數值梯度的優勢是比較精確,但缺點也很明顯,那就是速度較慢。因此我們通常求解代價函數的導函數來替換數值梯度,但導函數在人工實現時很容易出錯,我們已經實現了以極限方式的數值梯度求解,那么可以使用該函數檢驗實現的梯度函數。運行下列代碼,相對誤差應該小于1e-7。

其正確的結果應該如下所示。

2.6.4 向量化表達式計算損失函數及其梯度
完成顯式循環計算后,我們將損失函數與梯度的計算過程,使用完全向量形式再完成一遍。向量形式不僅書寫簡潔,并且也極大地加快了執行效率,對于編程人員來說,一開始使用向量化表達可能十分痛苦,但當慢慢熟悉后會對其著迷。打開“DLAction/classifiers/chapter2/softmax_loss.py”文件,文件實現了softmax_loss_vectorized函數。可以參考第1章中介紹的NumPy廣播的用法,不到山窮水盡,請不要偷看參考代碼。
提示:比如計算得分時,可以一次性求解所有訓練數據scores=np.dot(X,W),此時scores變成形狀為(數據個數,分類個數)的矩陣。輸入參數y為一個類標向量(數組),若y[i]=2就表示第i條數據的正確分類為2,但Softmax分類器生成的分類得分概率為一個矩陣(二維數組)。因此,需要將類標向量y轉換為one-hot(向量中類標位為1,其余為零),比如y[i]=2的類標,轉化為one-hot就為[0,0,1,0,0,0,0,0,0,0]。我們可以使用以下代碼進行one-hot形式的類標矩陣轉換:y_trueClass[range(num_train),y]=1.0,其形狀為(數據個數,分類個數),這樣就可以在后續的計算中使用向量計算。

接下來,使用前面已實現的softmax_loss_naive與向量化版本進行比較,完全向量化版本應該和顯式循環版本的結果相同,但前者的計算效率應該快得很多。運行下列代碼進行代碼檢驗。

其檢驗結果大致應該如下所示。

2.6.5 最小批量梯度下降算法訓練Softmax分類器
完成了Softmax的核心代碼后,接下來我們就使用最小批量梯度下降算法訓練Softmax分類器。在訓練階段,該過程十分簡單,基本上就是采樣數據,然后調用Softmax函數計算梯度,之后再更新權重,然后重復上述執行過程。如下所示,為該過程的算法偽代碼。

需要注意的是,我們的計數是從0開始的,10分類任務其y的最大值為9,因此num_classes=np.max(y)+1。在采樣時,重復采樣或非重復采樣都可以接受,但重復采樣的執行效率要高些,可以使用重復采樣加快執行效率,并且其對于梯度影響可以忽略不計。接下來就打開“DLAction/classifiers/chapter2/softmax.py”文件,對train()函數進行代碼填充工作。

如果編碼順利,那損失函數應該會隨著迭代次數的增加而減少。運行以下代碼塊,檢驗實現是否正確。

其結果如下所示。

為了更直觀地說明,我們將損失函數可視化并觀察其變化情況,代碼如下所示,損失函數如圖2-8所示。


圖2-8 Softmax損失函數變化情況
接下來我們編寫Softmax的預測代碼塊。在預測階段,我們不需要進行歸一化概率,僅僅輸出最高分所對應的類標號即可,該過程應該會很簡單。一行代碼就可以完成。

完成上述代碼后,就可以測試效果,我們輸出訓練數據量、驗證數據量、訓練正確率和驗證正確率。其代碼塊及輸出結果如下所示。

通過結果我們發現,訓練精度和驗證精度都不高,并且還出現了過擬合現象,接下來我們就開始使用超參數進行調整,訓練出一個最佳模型。
2.6.6 使用驗證數據選擇超參數
深度學習工程師,開個玩笑,也可以被稱為“調參工程師”,我們有太多的超參數可以選擇。就目前而言,學習率、權重衰減懲罰因子、批量大小和迭代次數都可以稱為超參數。超參數對最終的訓練結果影響顯著,并且不同的超參數組合,其結果也千差萬別。接下來我們將使用學習率以及權重衰減因子作為超參數進行選擇,請讓你的機器飛起來吧!
我們將固定批量大小以及迭代次數,其中batch_size=50,num_iters=300。
對于學習率和懲罰因子,使用逐步縮小范圍的方式,來挑選超參數。其學習率為:learning_rates=np.logspace(-9,0,num=10)。

懲罰因子為:regularization_strengths=np.logspace(0,5,num=10)。

其完整的訓練代碼和訓練結果如下所示。


為了更直觀地展示訓練結果,我們將訓練精度以及驗證精度進行可視化比較,使用顏色表示訓練性能,顏色越紅則效果越好,顏色越藍則效果越差。以下為可視化結果的代碼塊,圖2-9為可視化訓練結果。


圖2-9 可視化訓練結果一
從可視化結果可以清晰地看出,學習率大約在[1e-6,1e-4]之間效果顯著,懲罰因子在[1e0,1e4]之間效果較好,并且懲罰因子越小可能效果越好。因此我們下一步就在這個范圍內繼續縮小。代碼如下所示,圖2-10是新的訓練結果。


圖2-10 可視化訓練結果二
我們再次設置選擇范圍,如下所示,新的訓練結果如圖2-11所示。


圖2-11 可視化訓練結果三
此時我們的訓練精度幾乎都接近了100%,但最佳的驗證精度也只有0.31。那最終的測試結果如何呢?如下所示是我們的Softmax測試結果代碼塊,非常令人沮喪,測試集精度只有0.215。

實在不好意思,這是惡趣味的作者故意耍的一個小花招,那你知道問題出在哪嗎?如果你足夠認真和細心的話,可能早就發現了,那就是我們訓練的數據量實在太小了。上述的測試中,我們僅僅使用了250條數據進行訓練,不管我們如何優化,過擬合情況都很難避免,之所以這么做,是想讓你注意一個微小而又重要的知識,那就是數據量的問題。
當數據較少時,過擬合風險就越高,即使非常小心地選擇超參數,其效果也不會好,這是一個令人傷感的消息。但其實也有一個好消息,那就是當數據量足夠大時,過擬合現象就很難發生,這就使得訓練數據、驗證數據與測試數據之間的差值會越來越接近。在某些數據量特別巨大的情況下,可能只需關注如何將訓練數據錯誤率降到足夠低即可。超參數的選擇是一個非常耗時、耗力的過程,因此我們可以先使用較小的數據去粗略地選擇超參數的取值范圍,這樣可以節約訓練時間。但在較小數據中表現最好的超參數,不一定在數據量較大時同樣表現得更好,因此需要注意數據量的把控,但這是一個非常依賴于經驗的問題,需要從大量的實驗中自己積累經驗,進行不斷地嘗試。記住,千萬不要怕錯。
現在我們將重新載入數據集,使用完整的49000條數據進行訓練,1000條數據作為驗證?,F在舞臺交給你了,請盡一切可能訓練出一個最好的測試結果,你可以盡可能地調整現在所提供的4種超參數,如果計算能力和自己的耐心允許,也可以嘗試下交叉驗證。如果你足夠努力,你的測試正確率可以超過0.35。重新載入數據代碼塊和訓練代碼塊分別如下所示。


另一種更直觀檢驗模型好壞的方法是可視化模型參數,比如我們識別圖片“馬”,那模型的參數其實就是圖片的“馬模板”,也可以通過可視化參數去反推數據情況。比如我們的圖片中如果存在大量“馬頭向左”與“馬頭向右”的圖片,那訓練出來的模型參數很可能就變成了“雙頭馬”。同理,如果圖片中汽車的顏色大多數都是紅色,那訓練出的模型就可能是一輛“紅車模板”??梢暬瘏荡a塊如下所示,可視化訓練參數示意圖如圖2-12所示。


圖2-12 可視化訓練參數