2018年4月8日 星期日

使用 Keras 卷積神經網路 (CNN) 辨識手寫數字

三月初做完 MLP 測試後, 接著就開始看 CNN (卷積神經網路), 但原理部分需要時間消化, 加上又分心去學習 Node.js, 使得機器學習的進度緩了下來. 好消息是, 兩周前在鄉下的圖書館挖到寶-找到文魁出版的 "TensorFlow 之外的好選擇 : Keras Caffe SK-Learn 機器學習實作" :
Source : 天瓏

此書為大陸簡體書的繁體版, 內容比林大貴寫的 "TensorFlow+Keras 深度學習人工智慧實務應用" 在原理說明上要詳細些, 雖然數學不多, 但有些章節也不是我這初學者看一遍就能懂的. 此書除了講 Keras 外還介紹了 Caffe, 這是以 Shell Script + 設定檔為主的框架, 比起 Keras 的過度包裝 (但簡單好用), Caffe 在模型參數選項較詳細較自由, 執行速度也較快. 等 Keras 摸熟了再來試試 Caffe.

此書範例程式可在 GitHub 下載 (作者阿布微博 : abu_quant) :

# http://github.com/bbfamily/abu


本系列之前的測試紀錄參考 :

Windows 安裝深度學習框架 TensorFlow 與 Keras
使用 Keras 測試 MNIST 手寫數字辨識資料集
使用 Keras 多層感知器 MLP 辨識手寫數字 (一)
使用 Keras 多層感知器 MLP 辨識手寫數字 (二)

卷積神經網路 CNN 是法國人 Yann LaCun (揚-勒丘恩) 於貝爾實驗室任職時所提出, 是一種深度學習結構, 他也是在那時蒐集與建立了 MNIST 手寫辨識資料集. 卷積 (Convolution, 又譯為旋積或摺積) 是一種線性運算,  兩個函數 f 與 g 之卷積是將 f 與經過平移與翻轉的 g 相乘後生成的新函數 f*g. 若將 g 視為一個加權函數, 則卷積 f *g 相當於 f 與 g 的滑動加權總和. 卷積與傅立葉轉換有密切關係, 在快速傅立葉轉換中, 兩個離散訊號在時域的卷積相當於它們的傅立葉轉換在頻域的乘積, 參考 :

# Wiki : 卷積
# Wiki : 深度學習

而卷積神經網路 CNN 則是將離散卷積運算應用在人工神經網路, 藉由導入多個卷積層擷取空間特徵來提升辨識率, 透過隨機產生的卷積核心 (Kernel) 將一張圖片轉換成多張圖片, 這些卷積核心如同濾鏡 (filter) 一般, 卷積運算相當於將原始圖片通過不同濾鏡的過濾效果來提取或突顯各種空間特徵, 例如邊緣與輪廓等等資訊, 參考 :

Wiki : 卷積神經網絡
卷積神經網路的運作原理

下列影片演示了卷積神經網路運作之原理 :

How Convolutional Neural Networks work




多層卷積層神經元對於具體特徵不明確的影像或語音辨識很有用, CNN 的每一層都在做模式比對, 後面的 Layer 是對前面 Layer 比對出來的 Pattern 再做比對, 使得特徵越來越具體. 在監督式學習中, 通過 Pre-training 將樣本的特徵編碼為各層神經元中的權重 (weight) 與偏權值 (bias), 所謂的學習其實就是一種編碼 (Encoding) 動作 (稱為 Autoencoder), 將輸入訊息的特徵儲存於網路參數裡, 是對資料表達形式的一種結構性轉換.

但是多層結構也會產生過度擬合 (Overfitting) 與運算量膨脹問題, 需要 Dropout (部分丟棄) 與 Denoising (去雜訊) 來彌補, 參考 Fukuba (林志傑) 的學習筆記 :

林軒田教授機器學習技法 MACHINE LEARNING TECHNIQUES 第 13 講學習筆記

卷積神經網路主要是在分類模型之前導入卷積層與池化層. 卷積層用來對圖像進行卷積運算, 卷積運算保留了圖像的位置關係; 而池化層則用來降低圖像解析度, 減少後面神經網路之運算量. 具體的離散卷積運算, 在齋藤康毅寫的 "Deep Learning" 一書中有一個簡明的範例, 使用兩個矩陣說明如何計算其卷積 :




左邊的 4*4 矩陣為簡化的二維圖像資料, 右邊的 3*3 矩陣為一種濾鏡 (稱為卷積核心), 它攜帶了卷積運算所需之參數. 卷積運算是讓濾鏡在圖像矩陣內以固定間隔移動 (稱為 stride, 步幅), 並與重疊之圖像做積和運算, 亦即相對元素相乘後結果相加, 以上面範例來說, 3*3 濾鏡只能在 4*4 圖像矩陣中移動 4 次, 因此計算結果是一個 2*2 矩陣. 注意, 不要把矩陣的卷積與內積搞混了, 內積是列乘行之積和運算, 卷積是重疊對應元素之積和運算.

注意, 作為卷積核心 (Kernel) 的濾鏡通常為單數因次, 例如 3*3, 5*5, 7*7 ... 等, 雖然這並非必要 (複數因次的濾鏡也是可以運算), 但二維圖像在做卷積運算的重疊與平移時需要一個卷積中心點, 複數因次沒有一個中心點, 要單數因次才有.

卷積計算步驟如下 :

首先將濾鏡套在圖像的左上角, 然後將重疊的 3*3 矩陣相對元素做積和運算得到 15 :

第一列 : 1*2+2*0+3*1=5
第二列 : 0*0+1*1+2*2=5
第三列 : 3*1+0*0+1*2=5

全部相加和為 15. 接著將濾鏡向右移動一格 (stride=1),  卷積運算值為 16 :

第一列 : 2*2+3*0+0*1=4
第二列 : 1*0+2*1+3*2=8
第三列 : 0*1+1*0+2*2=4

然後將濾鏡向下移動一格到左下角,  卷積運算值為 6 :

第一列 : 0*2+1*0+2*1=2
第二列 : 3*0+0*1+1*2=2
第三列 : 2*1+3*0+0*2=2

最後將濾鏡向右移動一格到右下角,  卷積運算值為 15 :

第一列 : 1*2+2*0+3*1=5
第二列 : 0*0+1*1+2*2=5
第三列 : 3*1+0*0+1*2=5

不過上面這個計算卷積的方式會造成資料衰減, 讓原來的 4*4 圖像資料解析度縮減為 2*2, 經過多層卷積運算後將失去部分特徵. 為了不在卷積運算中喪失資訊, 可以在原圖像周圍先進行 "填補" (Padding, 填補值一般用 0) 以擴充解析度, 例如上面的 4*4 圖像經過四周填補一格即變成 5*5 解析度 :


濾鏡在此經過填補後的 5*5 圖像內以步幅 Stride=1 遊走進行卷積計算, Padding 的部分以 0 填補 , 得到的結果圖像解析度還是一樣 4*4, 計算程序如下所示 :



















可見使用這種填補方式進行步幅為 1 的卷積濾鏡運算後得到的圖像矩陣解析度仍然是 4*4, 這種 Stride=1 的移動方式在 Keras 中稱為 "same" 模式. 利用不同濾鏡從原始圖像就可以產生解析度一樣的多張圖像, 分別呈現原始圖像的各種空間資訊, 例如強化輪廓等等.

注意, 維持解析度不變的條件是步幅為 1 格, 上述運算中如果使用 2 格步幅的話, 卷積運算後將得到 2*2 的圖像輸出. 在齋藤康毅寫的 "Deep Learning" 一書中有列出輸入解析度 (W, H), 填補格數 P, 濾鏡大小 (FW, FH), 步幅 S, 與輸出解析度 (OW, OH) 之關係如下 :

OW=(W + 2P - FW) / S + 1
OH=(H + 2P - FH) / S + 1

例如上面的例子, 輸入解析度 (W=4, H=4), 填補格數 P=1, 濾鏡大小 (FW=3, FH=3), 步幅 S=1, 因此輸出解析度 OW=(4 + 2 * 1 - 3) / 1 + 1=4, OH=(4 + 2 * 1 - 3) / 1 + 1=4.

除了卷積層外, 卷積神經網路還導入池化層 (Pooling layer), 其主要目的是 :
  1. 減少資料量與參數數目, 從而減少計算量與過擬合 (Over fitting) 現象
  2. 降低圖像之位置差異
簡而言之, 池化層最主要的目的就是降低圖像資料的解析度來減少計算量, 亦即對原圖進行縮減取樣 (Down-sampling). 方法是利用一個稱為池化核心 (Pooling kernel) 的矩陣疊在原圖上, 如同卷積層的卷積核心 (濾鏡) 一樣在原圖中平移並計算輸出圖像, 步幅則為池化核心的寬度或高度 (即核心平移時不重疊). 注意, 池化核心不攜帶參數, 而是攜帶最大值或平均值等運算. 

常用的池化計算為 Max pooling (最大池化), 亦即在池化核心範圍內選取最大值輸出. 例如解析度 28*28 的圖像使用 2*2 的池化核心將得到 14*14, 也就是解析度降為原來的 1/4 (由 784 個畫素降為 196 個). 池化函數有多種, 除了最大池化外, 還有平均池化 (Average pooling), 亦即在池化核心範圍內計算圖像平均值作為輸出. 最大池化會突出最顯著的特徵像素; 而平均池化則會使圖像勻化.

以上面卷積層的輸出為例, 經過 2*2 池化核心的最大池化與平均池化 (相加後除以 4 再取四捨五入) 的結果如下 :





可見最大池化會挑出比較突出之圖像特徵; 而平均池化則是產出較勻化之輸出. 池化層除了是將圖像解析度縮減外還有兩個特性 :
  1. 池化層只是進行簡單的最大值運算, 故沒有學習參數
  2. 池化層對輸入資料的微小差異很穩健 (robust)
所謂穩健的意思就是抗雜訊效果, 例如將上面的圖像資料向右平移一格並填入其他值, 最大池化結果僅一個或兩個有變化, 甚至可能完全不變, 例如 :





上面第一個只有左上角的畫素不同, 而第二個則是池化後完全不變.

以上關於 CNN 卷積層與池化層的結構摘要整理如下 :
  1. 卷積核心的尺寸通常為單數; 而池化核心的尺寸通常為偶數.
  2. 卷積核心有攜帶參數進行積和運算; 池化核心不帶參數只帶最大值或平均值運算.
  3. 卷積核心平移之步幅為 1 (重疊); 池化核心平移之步幅為其尺寸 (不重疊).
深度學習使用多重的卷積-池化層來深化特徵擷取, 在林大貴寫的 "TensorFlow+Keras 深度學習人工智慧實務應用" 第 8 章使用雙重的卷積-池化層來擷取 MNIST 手寫數字資料集圖片之空間特徵. 兩個卷積層都使用 ReLu 非線性激活函數, 其結構如下 :




上圖描述雙重的卷積-池化層模型結構, 輸入層為 28*28 解析度之灰階 MNIST 圖片, 經過卷積層 1 隨機產生的 16 個 5*5 濾鏡 (卷積核心) 進行卷積運算後產生 16 個 28*28 解析度之圖片 (步幅=1 解析度不變), 然後經過池化層 1 以 2*2 的最大值池化核心將 16 張 28*28 解析度圖片轉成 16 張 14*14 解析度圖片 (步幅=2, 尺寸減半, 解析度降為 1/4). 

卷積層 2 則用隨機產生的 36 個 5*5 濾鏡 (卷積核心) 對池化層 1 輸出的 16 張 14*14 解析度圖片進行卷積運算, 這會產生 36 個 14*14 解析度之圖片 (步幅=1 解析度不變), 然後經過池化層 2 以 2*2 的最大值池化核心將 36 張 14*14 解析度圖片轉成 36 張 7*7 解析度圖片 (步幅=2, 尺寸減半, 解析度降為 1/4). 池化層的輸出經過 flatten() 函數全部展成 1*1764 的一維特徵向量, 然後送進 MLP 全連接神經網路分類模型進行學習與辨識, 結構如下 : 




注意, 此處第一層改稱為平坦層, 因為輸入層已經往前移到卷積層 1 了. 由於池化層 2 輸出 36 張 7*7 的圖片, 經 flatten() 展開後變成 1*1764 的一維向量, 因此平坦層為 1764 個神經元, 使用 ReLu 激活函數; 隱藏層則改為 128 個神經元, 使用 Softmax 激活函數到輸出層.

以下就根據林大貴的 "TensorFlow+Keras 深度學習人工智慧實務應用" 第 8 章內容來測試 CNN 網路的 MNIST 辨識, 其程序與之前 MLP 測試一樣 :
  1. 資料預處理 (Reshape 為 28*28 單色)
  2. 建立模型 (雙重卷積-池化層 + 平坦層 + 隱藏層 + 輸出層)
  3. 訓練模型 (使用 60000 訓練集)
  4. 評估模型準確率 (使用 10000 訓練集)
  5. 進行預測 (使用 10000 訓練集)
注意, 在上兩篇 Keras 的 MLP 測試中, 預處理階段是將每一張 28*28 灰階解析度的 MNIST 圖片利用 reshape() 函數拉平為 1*768 的一維向量, 這樣其實會失去一些圖像特徵. 在 CNN 中則是利用卷積層來擷取圖片之空間特徵, 因此饋入卷積層的圖片資料必須保持原本的 28*28 解析度, 不可拉平.

以下為 CNN 測試紀錄, 此書範例程式可在博碩網站下載 (IPython 範例檔) :

www.drmaster.com.tw/download/example/MP21710_example.zip



1. 匯入套件與資料集 : 

首先是匯入 keras 的 Numpy 工具集, Numpy 套件, 以及 MNIST 手寫辨識資料集 :

D:\test>python
Python 3.6.1 (v3.6.1:69c0db5, Mar 21 2017, 18:41:36) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> from keras.utils import np_utils     #匯入 Keras 的 Numpy 工具 
Using TensorFlow backend.
>>> import numpy as np                         #匯入 Numpy
>>> np.random.seed(10)                          #設定隨機種子, 以便每次執行結果相同
>>> from keras.datasets import mnist    #匯入 mnist 模組後載入資料集
>>> (x_train_image, y_train_label), (x_test_image, y_test_label)=mnist.load_data()

函數 mnist.load_data() 傳回兩組 tuple (其元素均為陣列), 前者為 60000 筆之訓練資料集之圖片與標籤, 後者為 10000 筆測試資料集之圖片與標籤. 圖片皆為 28*28 的二維陣列, 用來表示一張解析度為 28*28 的灰階圖片, 每一個元素代表 0~255 的畫素值. 標籤為一維陣列, 表示圖片所對應之答案 (0~9 之整數), 這些資訊都放在陣列之 shape 屬性中 :

>>> x_train_image.shape
(60000, 28, 28)
>>> x_test_image.shape 
(10000, 28, 28)
>>> y_train_label.shape 
(60000,)
>>> y_test_label.shape 
(10000,)

可見圖片陣列的 shape 屬性值為三元素的 tuple, 第一個元素表示此陣列的元素個數, 訓練集有 60000 個; 測試集有 10000 個. 第二與第三元素表示此二維陣列因次為 28*28, 分別為 x 軸與 y 軸畫素個數. 用索引即可取得圖片內容, 例如 x_train_image[0] 為訓練集第一張圖片; x_train_image[59999] 為訓練集最後一張圖片. 標籤陣列為一維陣列, 故僅有一個元素, 訓練集有 60000 個標籤; 測試集有 10000 個標籤. 用索引即可取得標籤內容, 例如 y_train_label[0] 為訓練集第一張圖片之標籤;  y_train_label[59999] 為訓練集最後圖片之標籤.


2. 資料預處理 :

接著是呼叫 reshape() 函數將陣列轉換成 float32 浮點數, 此處與之前 MLP 時將圖片轉成一維向量拉平作法不同, 為了保存圖片的空間資訊必須保留其二維結構, 因此 reshape() 需傳入 4 個參數 : 第一參數仍是陣列元素個數, 第二與第三參數是二維陣列之因次 (即列與行解析度), 第四參數是色版數目, 因 MNIST 資料集是單色灰階, 故傳入 1 :

>>> x_train=x_train_image.reshape(60000,28,28,1).astype('float32') 
>>> x_test=x_test_image.reshape(10000,28,28,1).astype('float32') 

接下來與 MLP 一樣是用除以畫素最大值 255 的方法將圖片正規化 (Normalization), 亦即 0~255 的像素值就全部變成 0~1 之值了; 標籤部分則呼叫 Keras 的 np_utils.to_categorical() 函數將 0~9 的數值經過 One-hot encoding 編碼 (獨熱編碼) 轉成 10 位元二進碼, 例如 5 變成  0000010000 :

>>> x_train=x_train_image.reshape(60000,28,28,1).astype('float32')
>>> x_test=x_test_image.reshape(10000,28,28,1).astype('float32') 
>>> x_train_normalize=x_train/255
>>> x_test_normalize=x_test/255
>>> y_train_onehot=np_utils.to_categorical(y_train_label)
>>> y_test_onehot=np_utils.to_categorical(y_test_label)

這樣資料預處理就完成了.


3. 建立 CNN 模型

接下來是利用 Keras 的線性堆疊模型 Sequential 一層層地建構 CNN 卷積神經網路與其分類模型. 首先須匯入 Sequential 模組, 平面卷積模組 Conv2D, 平面池化模組 MaxPooling2D, 完全連接模組 Dense, 以及放棄模組 Dropout :

>>> from keras.models import Sequential 
>>> from keras.layers import Dense, Dropout, Flatten, Conv2D, MaxPooling2D 

然後呼叫 Sequential() 建立空的模型物件, 再用其 add() 方法將卷積層 1 與池化層 1, 使用 16 個5*5 的隨機卷積核心 (濾鏡), 以 "same" 邊界模式將一張 28*28 圖片產生 16 層 28*28 圖片 (same 方式不改變影像大小), 並使用 ReLu 非線性函數激活, 然後用 2*2 的池化核心將其解析度降為 14*14 :

>>> model=Sequential()                            #建立空的線性堆疊模組
>>> model.add(Conv2D(filters=16,          #加入卷積層 1 (16 個隨機卷積核心)
...                  kernel_size=(5,5),                                  #卷積核心尺寸 5*5
...                  padding='same',                                    #邊界模式=same (填補=0, 步幅=1)
...                  input_shape=(28,28,1),                          #輸入圖片為 28*28 單色
...                  activation='relu'))                                  #激活函數=ReLu
>>> model.add(MaxPooling2D(pool_size=(2, 2)))   #加入池化層 1 (池化核心 2*2)

然後加入卷積層 2 與池化層 2, 使用 36 個5*5 的隨機卷積核心 (濾鏡), 以 "same" 邊界模式從 16 層之 14*14 圖片產生 36 層的 14*14 圖片 (same 方式不改變影像大小), 並使用 ReLu 非線性函數激活, 然後用 2*2 的池化核心將其解析度降為 7*7 :

>>> model.add(Conv2D(filters=36,           #加入卷積層 2 (36 個隨機卷積核心)
...                  kernel_size=(5,5),                                   #卷積核心尺寸 5*5
...                  padding='same',                                     #邊界模式=same (填補=0, 步幅=1)
...                  activation='relu'))                                   #激活函數=ReLu
>>> model.add(MaxPooling2D(pool_size=(2, 2)))        #加入池化層 2 (池化核心 2*2)

不過這裡有個疑問, 卷積層 2 是如何從 16 層的池化層 1 圖像輸出產生 36 層的卷積層 2 輸出? 由於實際做法被 Keras 封裝起來無法探知, 只能以後再研究了.

接下來為了避免過擬合 (Over fitting) 問題, 最後加上 Dropout 層在每次訓練中放棄部分神經元 (此處為放棄 25% 神經元) :

>>> model.add(Dropout(0.25)) 

這樣便完成了 CNN 網路模型之建置.


4. 建立分類模型 : 

上面經過 CNN 網路學習得到之圖像空間特徵需經過分類模型辨識圖片屬於 0~9 的哪一個, 使用的分類模型是之前的 MLP 多層感知器, MLP 為全連接網路, 是 DNN (Deep Neural Network) 網路的一種. 除了 MLP 外, 也可以使用其他分類模型例如隨機森林等.

此處所用之 MLP 分類模型為三層模型 :
  1. 平坦層 
  2. 隱藏層 
  3. 輸出層 
首先呼叫 Flatten() 建立平坦層, 由於前面 CNN 模型最後池化層 2 之輸出為 36 層的 7*7 圖像, 因此平坦層會將其依序拉平展開為 36*7*7=1764 個一維特徵向量 (畫素), 並建立 1764 個神經元來接收這些特徵值.

其次呼叫 Dense() 建立隱藏層, 具有 128 個以 ReLu 為激活函數的神經元. 同樣地為避免過擬合, 在隱藏層後面會加上一個 Dropout 層, 此處設為 0.5 表示在每次訓練時會放棄 50% 的神經元.

最後呼叫 Dense() 建立輸出層, 具有 10 個以 Softmax 為激活函數之輸出神經元, 其 One-hot 的 0/1 輸出代表 0~9 之數字識別結果 :

>>> model.add(Flatten())                                      #建立平坦層 (36*7*7=1764 個神經元)
>>> model.add(Dense(128, activation='relu'))     #建立隱藏層 (128 個神經元)
>>> model.add(Dropout(0.5))                                 #放棄 50% 神經元
>>> model.add(Dense(10,activation='softmax'))  #建立輸出層 (10 個神經元)


5. 顯示模型摘要 :

建立好 CNN 與分類模型之後可呼叫模型之 summary() 顯示摘要如下 :

>>> print(model.summary())                                  #顯示摘要

_________________________________________________________________
Layer (type)                 Output Shape              Param #
=================================================================
conv2d_1 (Conv2D)            (None, 28, 28, 16)        416
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 14, 14, 16)        0
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 14, 14, 36)        14436
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 7, 7, 36)          0
_________________________________________________________________
dropout_1 (Dropout)          (None, 7, 7, 36)          0
_________________________________________________________________
flatten_1 (Flatten)          (None, 1764)              0
_________________________________________________________________
dense_1 (Dense)              (None, 128)               225920
_________________________________________________________________
dropout_2 (Dropout)          (None, 128)               0
_________________________________________________________________
dense_2 (Dense)              (None, 10)                1290
=================================================================
Total params: 242,062
Trainable params: 242,062
Non-trainable params: 0
_________________________________________________________________
None

從摘要表可知只有卷積層, 隱藏層與輸出層有參數, 而池化層與放棄層都是沒有參數的.


6. 進行神經元訓練 :

模型建好後即可進行訓練 (使用 60000 筆訓練集), 訓練前須呼叫 compile() 函數進行設定 :

>>> model.compile(loss='categorical_crossentropy', 
...               optimizer='adam',metrics=['accuracy']) 

傳入三個參數 :

loss : 損失函數使用交叉熵
optimizer : 訓練時之最佳化方法使用 "adam" (快速收斂且準確度高)
metrics : 評估模型之方式為 "accuracy" (準確度)

設定好訓練參數後即可呼叫 fit() 函數開始訓練, 方式是從 60000 筆訓練集中取 80% (48000 筆) 做訓練, 20% (12000 筆) 做驗證, 共執行 10 次訓練週期 (epoches), 每次訓練時並非一次將 48000 訓練集全部丟進去, 而是分批次, 每批次取 300 筆資料, 因此 48000 筆要分 160 批次才完成一個epoch (訓練週期 ). 注意, 以 CPU 執行 fit() 較耗時, 我的桌上型電腦跑了大約  8 分鐘 :

>>> train_history=model.fit(x=x_train_normalize,
...                         y=y_train_onehot,validation_split=0.2, 
...                         epochs=10, batch_size=300,verbose=2)

此處傳入 6 個參數 :

x=正規化後的 28*28 圖片特徵向量
y=One-hot 編碼的圖片標籤 (答案)
validation_split=驗證資料集占訓練集之比率
epochs=訓練週期
batch_size=每一批次之資料筆數
verbose=顯示選項 (2=顯示訓練過程)

十個 epoches 的訓練過程如下 :

Train on 48000 samples, validate on 12000 samples
Epoch 1/10
2018-04-07 10:46:32.609924: I C:\tf_jenkins\workspace\rel-win\M\windows\PY\36\tensorflow\core\platform\cpu_feature_guard.cc:137] Your CPU supports instructions that this TensorFlow binary was not compiled to use: AVX
 - 216s - loss: 0.4859 - acc: 0.8479 - val_loss: 0.0965 - val_acc: 0.9728
Epoch 2/10
 - 215s - loss: 0.1409 - acc: 0.9586 - val_loss: 0.0629 - val_acc: 0.9810
Epoch 3/10
 - 182s - loss: 0.1017 - acc: 0.9694 - val_loss: 0.0514 - val_acc: 0.9841
Epoch 4/10
 - 183s - loss: 0.0841 - acc: 0.9752 - val_loss: 0.0463 - val_acc: 0.9859
Epoch 5/10
 - 186s - loss: 0.0715 - acc: 0.9782 - val_loss: 0.0396 - val_acc: 0.9875
Epoch 6/10
 - 185s - loss: 0.0638 - acc: 0.9809 - val_loss: 0.0401 - val_acc: 0.9877
Epoch 7/10
 - 192s - loss: 0.0556 - acc: 0.9832 - val_loss: 0.0428 - val_acc: 0.9873
Epoch 8/10
 - 185s - loss: 0.0503 - acc: 0.9846 - val_loss: 0.0339 - val_acc: 0.9891
Epoch 9/10
 - 179s - loss: 0.0456 - acc: 0.9863 - val_loss: 0.0340 - val_acc: 0.9897
Epoch 10/10
 - 178s - loss: 0.0431 - acc: 0.9866 - val_loss: 0.0339 - val_acc: 0.9902

與之前 MLP 的訓練結果 (約 0.9765) 比較 , 準確率提高了 1.4%. 十個 epoches 的訓練結果會以 loss (訓練集損失), acc (訓練集準確度), val_loss (驗證集損失), 以及 val_acc (訓練集準確度) 為鍵儲存在 dict 型態變數中回傳, 查詢訓練結果如下 :

>>> train_history.history["loss"]   
[0.48587896302342415, 0.14091337535064669, 0.10172861367464066, 0.08413523898925632, 0.07145443321787752, 0.06379812054801733, 0.05555043590720743, 0.05027689200942405, 0.04557928261347115, 0.043080858269240706]
>>> train_history.history["acc"]   
[0.8479166681412608, 0.9585624933242798, 0.9693541698157787, 0.9751666773110628, 0.9782291784882545, 0.9809375133365392, 0.9831875145435334, 0.9846250142902135, 0.9862500127404928, 0.9866041786968708]
>>> train_history.history["val_loss"]   
[0.0964868551120162, 0.06290977750904858, 0.05137179638259113, 0.04628049440216273, 0.03960396841866896, 0.040075597888790074, 0.042766022717114535, 0.03386718961992301, 0.034033434221055356, 0.03393690842203796] 
>>> train_history.history["val_acc"] 
[0.9727500036358834, 0.9810000091791153, 0.9840833440423011, 0.9859166786074638, 0.9875000104308128, 0.9876666769385338, 0.9873333424329758, 0.989083343744278, 0.9896666765213012, 0.9901666760444641]

可使用前次測試 MLP 時所寫的 matplotlib 函數來繪製訓練紀錄, 描繪誤差值 (loss) 與準確度 (acc) 隨 epoch 變化的情形 :

>>> import matplotlib.pyplot as plt 
>>> def show_train_history(train_history):
...     fig=plt.gcf()
...     fig.set_size_inches(16, 6)
...     plt.subplot(121)
...     plt.plot(train_history.history["acc"])
...     plt.plot(train_history.history["val_acc"])
...     plt.title("Train History")
...     plt.xlabel("Epoch")
...     plt.ylabel("Accuracy")
...     plt.legend(["train", "validation"], loc="upper left")
...     plt.subplot(122)
...     plt.plot(train_history.history["loss"])
...     plt.plot(train_history.history["val_loss"])
...     plt.title("Train History")
...     plt.xlabel("Epoch")
...     plt.ylabel("Loss")
...     plt.legend(["train", "validation"], loc="upper left")
...     plt.show()
...
>>> show_train_history(train_history) 




可見隨著訓練週期增加, 誤差值越來越低, 而準確率越來越高.


7. 評估模型準確率 : 

以 60000 筆訓練集完成模型的 10 個訓練週期後, 最後的準確度可達 99%, 接下來可呼叫 evaluate() 函數來評估此訓練過的模型對 10000 筆測試集之準確度有多少 ? 傳入參數為測試集之正規化數字圖片以及其 onehot 編碼之標籤, 傳回值型態為串列, 其中準確率放在索引 1 :

>>> scores=model.evaluate(x_test_normalize, y_test_onehot)   
10000/10000 [==============================] - 16s 2ms/step
>>> print("Accuracy=", scores) 
Accuracy= [0.02592632946753947, 0.9909] 
>>> print("Accuracy=", scores[1]) 
Accuracy= 0.9909 

可見與訓練集 10 週期訓練結果之 0.9902 相近.


8. 以測試集進行預測 : 

最後就是呼叫 predict_classes() 傳入正規化後的測試集陣列 (10000 筆資料) 進行預測, 結果會放在一個陣列中傳回 :

>>> prediction=model.predict_classes(x_test_normalize) 
>>> print(prediction)     #只列出前後各 3 個辨識結果
[7 2 1 ... 4 5 6]                         
>>> prediction[0]            #用索引逐一查詢辨識結果陣列
7
>>> prediction[1]
2
>>> prediction[2]
1
>>> prediction[9997]
4
>>> prediction[9998]
5
>>> prediction[9999]
6
>>> prediction[:5]
array([7, 2, 1, 0, 4], dtype=int64)
>>> prediction[9995:]
array([2, 3, 4, 5, 6], dtype=int64)


9. 顯示混淆矩陣 : 

使用 pandas 套件的 crosstab() 函數可用來建立混淆矩陣以觀察那些數字比較會被誤認, 傳入參數為測試集的 10000 個標籤 (答案) y_test_label 以及上面得到預測結果 prediction :

>>> import pandas as pd 
>>> pd.crosstab(y_test_label, prediction, rownames=['label'],colnames=['predict'])
predict    0     1     2     3    4    5    6     7    8    9
label
0        977     0     0     0    0    0    2     1    0    0
1          0  1130     1     0    0    1    1     1    1    0
2          2     0  1027     0    1    0    0     2    0    0
3          0     0     1  1004    0    3    0     2    0    0
4          0     0     0     0  974    0    1     0    1    6
5          1     1     0     3    0  884    2     0    0    1
6          5     2     0     1    1    1  948     0    0    0
7          0     1     3     3    0    0    0  1018    1    2
8          4     0     3     2    1    1    0     2  957    4
9          1     3     2     2    3    2    0     4    0  992

從對角線來看, 5 的辨識率最低, 只有 884 次, 最容易被混淆; 而 1 的辨識率最高, 達 1130 次. 另外, 4 最容易被誤認為 9 達 6 次, 其次是 6 被誤認為 0 達 5 次, 比起純粹使用 MLP 分類模型辨識, 誤認的次數都有降低, 因此辨識成功率提高了.




10. 利用 DataFrame 找出哪些測試樣本被誤認 :

上面的混淆矩陣顯示 4 被誤認為 6 次數最高 (6 次), 6 被誤認為 0 次高 (5 次), 可以利用 pandas 的 DataFrame 找出被誤認的測試樣本是哪些索引, 首先將測試集標籤 y_test_label 與預測結果 prediction 組成 DataFrame 物件, 利用欄位的條件式即可找出被誤認的測試樣本 :

>>> df=pd.DataFrame({'label':y_test_label, 'predict':prediction}) 
>>> df[(df.label==4) & (df.predict==9)]      #標籤為 4 被誤認為 9 者 (6 次)
      label  predict
740       4        9
1242      4        9
2130      4        9
4265      4        9
8520      4        9
9792      4        9
>>> df[(df.label==6) & (df.predict==0)]      #標籤為 6 被誤認為 0 者 (5 次)
      label  predict
259       6        0
445       6        0
965       6        0
2118      6        0
3422      6        0

我將以上指令整合為如下程式 :

#show_cnn_training_history_and_prediction.py
import sys
from keras.datasets import mnist
from keras.utils import np_utils
import matplotlib.pyplot as plt
import numpy as np
from keras.models import Sequential
from keras.layers import Dense,Dropout,Flatten,Conv2D,MaxPooling2D
import pandas as pd

def show_train_history(train_history):
    fig=plt.gcf()
    fig.set_size_inches(16, 6)
    plt.subplot(121)
    plt.plot(train_history.history["acc"])
    plt.plot(train_history.history["val_acc"])
    plt.title("Train History")
    plt.xlabel("Epoch")
    plt.ylabel("Accuracy")
    plt.legend(["train", "validation"], loc="upper left")
    plt.subplot(122)
    plt.plot(train_history.history["loss"])
    plt.plot(train_history.history["val_loss"])
    plt.title("Train History")
    plt.xlabel("Epoch")
    plt.ylabel("Loss")
    plt.legend(["train", "validation"], loc="upper left")
    plt.show()

#pre-processing
np.random.seed(10)
(x_train_image, y_train_label), (x_test_image, y_test_label)=mnist.load_data()
x_train=x_train_image.reshape(x_train_image.shape[0],28,28,1).astype('float32')
x_test=x_test_image.reshape(x_test_image.shape[0],28,28,1).astype('float32')
x_train_normalize=x_train/255
x_test_normalize=x_test/255
y_train_onehot=np_utils.to_categorical(y_train_label)
y_test_onehot=np_utils.to_categorical(y_test_label)

#create model
model=Sequential()
model.add(Conv2D(filters=16,
                 kernel_size=(5,5),
                 padding='same',
                 input_shape=(28,28,1),
                 activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=36,
                 kernel_size=(5,5),
                 padding='same',
                 activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(10,activation='softmax'))
print(model.summary())

#train model
model.compile(loss='categorical_crossentropy',
              optimizer='adam',metrics=['accuracy'])
train_history=model.fit(x=x_train_normalize,
                        y=y_train_onehot,validation_split=0.2,
                        epochs=10, batch_size=300,verbose=2)

#show test images with prediction
scores=model.evaluate(x_test_normalize, y_test_onehot)
print("Accuracy=", scores[1])
prediction=model.predict_classes(x_test_normalize)
print(prediction)
pd.crosstab(y_test_label, prediction,rownames=['label'],colnames=['predict'])
df=pd.DataFrame({'label':y_test_label, 'predict':prediction})
df[(df.label==6) & (df.predict==0)]
df[(df.label==4) & (df.predict==9)]


參考 :

熵 (Entropy)
# Numpy Quick Tutorial
吳老師教學中心 (Python & 機器學習)
# Manning : Deep Learning with Python
用tflearn來做深度學習辨識初音
# (台大李宏毅教授教學網站)
Deep learning for complete beginners: convolutional neural networks with keras
CNN(卷積神經網絡)、RNN(循環神經網絡)、DNN(深度神經網絡)的內部網絡結構的區別

沒有留言 :