Ch11卷積神經網路(Convolutional Neural Network)
卷積神經網路(Convolutional Neural Network,縮寫為CNN)屬於深度學習,常用於影像辨識,透過卷積(Convolution)將圖片中相鄰點一起運算,適合用於二維的影像資料,已經達成不錯的影像辨識效果。
11-1卷積神經網路模型運作原理
卷積神經網路使用前向傳播算法(Forward Propagation)產生新的預測結果,計算損失值後,使用反向傳播算法 (Backward Propagation)更新參數,每次更新參數目標為降低損失函式的數值,不斷使用前向傳播算法與反向傳播算法降低損失值,直到獲得最小損失值為止,這部分與神經網路運作原理相同。不同的是卷積神經網路,使用卷積方式運算,以下介紹卷積神經網路的運作原理。
卷積神經網路分成特徵擷取(Feature Extraction)和特徵分類(Feature Classification)兩個部分。特徵擷取由卷積層(Convolution Layer)與池化層(Pooling Layer)組成,可以重複使用多個卷積層與池化層組成,而特徵分類由攤平層(Flatten Layer)與全連接層(Fully Connected Layer)組成,最後輸出各個分類的機率。
(1)卷積層(Convolution Layer)
利用卷積層擷取影像的輪廓,可以使用3x3、5x5或7x7矩陣當成卷積核(Kernel)或稱為Filter擷取影像,卷積核就是卷積神經網路的參數。假設原始影像為5x5矩陣,使用3x3矩陣為卷積核,以下分別介紹strides設定為1或2的卷積運算詳細過程。
設定strides為1
strides設為1表示卷積核每次往右或往下移動1格,運算結果為3x3矩陣,卷積運算詳細過程如下。
Step1)求第一列第一行的運算結果,將原始影像左上角3x3矩陣與3x3的卷積核對應的元素相乘後相加,結果如下。
2*1+3*1+4*0+4*(-1)+5*0+4*1+0*0+2*(-1)+6*0=2+3-4+4-2=3
設定strides為2
此範例設定strides為2,卷積核每次往右或往下移動2格,運算結果為2x2矩陣,卷積運算詳細過程如下。
設定padding為same
Keras函式庫中padding預設為valid,也就是原始影像的四周不會填上0,執行卷積運算後圖形會縮小。若不想卷積運算後圖形縮小,或想加強圖像邊緣的辨識能力,設定padding為same,會在原始影像的四周填上0,5x5矩陣的原始影像會變成7x7矩陣。設定strides為1,使用3x3的卷積進行運算,可獲得5x5的矩陣,大小與原始影像相同。
設定激勵函式
假設使用激勵函式ReLU,則計算結果小於0的值更改為0,大於0的值維持不變,前一步驟卷積運算結果,經由激勵函式ReLU,將小於0的數值改為0,就是經由激勵函式ReLU的運算結果。
卷積核個數
一個卷積層可以使用多個卷積核,卷積核個數通常使用8個、16個、32個、64個、128個…等。每個卷積核可以有多個頻道(Channel),例如:彩色照片需要3個頻道,因為彩色由RGB三原色組成,3x3的卷積核需要3個,每個顏色對應一個卷積核,實際上卷積核為3x3x3。兩個卷積層串聯在一起時,上一層卷積層使用64個卷積核,則下一層卷積層的一個卷積核(假設大小為3x3)需要有64個頻道,也就是下一層卷積層的卷積核為3x3x64。卷積神經網路就是找到最佳的卷積核,卷積核就是卷積神經網路的參數,讓損失值降到最低,達成更精確的預測結果。
以手寫數字為例,第一個訓練資料為數字5如下,經過卷積層運算,使用3x3的卷積核(Kernel),設定strides為1,使用16個卷積核,設定padding為same,激勵函式為relu,輸出維度為28x28x16,產生右側16張圖片是16個卷積核運算結果,每張圖片大小為28x28。
(2)池化層(Pooling Layer)
池化層在指定範圍內的多個元素取最大值(MaxPooling)或取平均值(AveragePooling),可以選定2x2的矩陣範圍,設定strides為2,假設原始輸入為4x4矩陣,經由池化層的MaxPooling運算後只會剩下2x2矩陣。池化層的優點為可以降低下一層的輸入資料量,加快執行速度,且大部分情況下,並不會影響特徵擷取,對於影像辨識的結果影響不大。
以手寫數字為例,第一個訓練資料為數字5,經過卷積層,輸出維度為28x28x16,產生左側16張圖片,每張圖片大小為28x28,經由池化層感應範圍為2x2,設定strides為2,輸出維度為14x14x16,產生右側16張圖片,每張圖片大小為14x14。
(3)攤平層(Flatten Layer)
經由卷積層與池化層的特徵擷取後,需要使用全連接層產生分類的預估結果,全連接層使用一維陣列為輸入,使用攤平層將池化層產生的二維陣列轉換成一維陣列,再將一維陣列輸入下一層的全連接層。
(4)全連接層(Fully Connected Layer)
使用全連接層計算每一個分類的機率,取最高者為最後預測的分類。全連接層的神經元個數就是分類個數,假設辨識手寫數字0到9,全連接層需要10個神經元,一個神經元表示一個數字的機率,激勵函式使用函式softmax,函式softmax會讓10個神經元的輸出值加總為1。該數字可能性越大者,該數字表示的神經元輸出值越大,最後取出最大值所表示的數字,就是辨識結果。
下表以手寫數字7為例,最後數字7的機率接近100%,表示辨識正確。
綜合上述卷積神經網路概念,以辨識手寫數字為例,說明卷積神經網路的實作流程。每個手寫數字圖片為寬度28像素高度28像素,經由兩個卷積層與池化層進行特徵擷取,接著使用攤平層與全連接層計算各分類的機率,流程如下。
上述辨識手寫數字的卷積神經網路每一層參數個數如下。此為使用keras套件建立手寫數字辨識模型,輸出該模型的摘要結果。
各層參數個數說明如下:
(1)第一個卷積層(使用3x3的卷積核(Kernel),設定strides為1,使用16個卷積核,設定padding為same,輸出維度為28x28x16)。使用3x3的卷積核,手寫數字為黑白照片,所以頻道(Channel)數為1,卷積核大小為3x3x1,表示每一個卷積核由1個3x3矩陣組成,需要16個卷積核。參數個數為(3x3x1+1(bias))*16等於160,所以第一個卷積層參數個數為160。
(2)第一個池化層沒有卷積核,所以參數個數為0,輸出維度為14x14x16,由此可知下一層卷積層的卷積核頻道數為16。
(3)第二個卷積層(使用3x3的卷積核(Kernel),設定strides為1,使用16個卷積核,設定padding為valid,輸出維度為12x12x16)。使用3x3的卷積核,因為上一層池化層輸出維度為14x14x16,所以頻道(Channel)數為16,第二個卷積層的卷積核大小為3x3x16,表示每一個卷積核由16個3x3矩陣組成,需要16個卷積核。參數個數為(3x3x16+1(bias))*16等於2320,所以第二個卷積層參數個數為2320。
(4)第二個池化層沒有卷積核,所以參數個數為0,輸出維度為6x6x16。
(5)攤平層會把上一層(第二個池化層)的輸出,轉換成一維陣列,輸出維度為576,因為6x6x16等於576,維度轉換不需要參數。
(6)全連接層使用10個神經元,上一層有576個輸出,參數個數為5770,因為(576+1(bias))*10等於5770。
此模型參數總數為每一層參數個數相加,160+2320+5770等於8250,卷積神經網路模型就是調整這些參數,學會辨識手寫數字。
11-2 使用keras實作卷積神經網路
使用keras實作卷積神經網路,將每筆資料輸入卷積神經網路模型就可訓練模型,輸入測試資料到模型,就可以獲得預測結果,評估模型的正確率。使用keras實作卷積神經網路的步驟如下,以辨識手寫數字為例。
11-3 卷積神經網路實作範例
11-3-1 使用卷積神經網路辨識手寫數字
本範例使用手寫阿拉伯數字資料集(mnist),每個手寫數字由長寬各28個像素組成,將這些手寫數字分成數字0到數字9總共10類。
from tensorflow.keras.datasets import mnist
(train_X, train_y), (test_X, test_y) = mnist.load_data()
Step1)匯入資料
從keras.datasets的mnist匯入手寫數字資料。
程式說明
第1到7行:匯入函式庫。
第8行:設定繪圖模組plt的中文字型。
第9行:使用函式load_data匯入手寫數字資料集mnist的訓練資料到train_X與train_y,測試資料到test_X與test_y。
第10到11行:轉換train_X與test_X的維度,多出一個維度,才能符合輸入資料的規格,黑白圖片需要新增維度,彩色圖片不需要。
Step2)修改資料
將train_y與test_y改成one-hot編碼。
程式說明
第1行:使用函式to_categorical轉換train_y為one-hot編碼,使用變數train_y2參考到此轉換結果。
第2行:使用函式to_categorical轉換test_y為one-hot編碼,使用變數test_y2參考到此轉換結果。
第3到4行:使用shape顯示test_y與test_y2的維度大小。
可以發現test_y是一維陣列,有10000個元素,經由函式to_categorical轉換後,變成二維陣列,有10000列,每列有10個元素,相當於1個元素變10個元素。
one-hot編碼
one-hot編碼轉換過程如下,test_y的數值由0到9組成,假設test_y為0,則轉換成[1, 0, 0, 0, 0, 0, 0, 0, 0, 0];假設test_y為1,則轉換成[0, 1, 0, 0, 0, 0, 0, 0, 0, 0];假設test_y為2,則轉換成[0, 0, 1, 0, 0, 0, 0, 0, 0, 0];依此類推,假設test_y為9,則轉換成[0, 0, 0, 0, 0, 0, 0, 0, 0, 1],這就是one-hot編碼。
Step3)建立模型
使用Sequential建立模型,依序加入卷積層、池化層、攤平層與全連結層。
程式說明
第1行:使用Sequential建立卷積神經網路模型。
第2行:使用Conv2D新增二維的卷積層,使用3x3的卷積核(Kernel),使用16個卷積核,設定strides為1,設定padding為same,設定激勵函式為relu,輸入手寫圖片維度為28x28x1。
第3行:使用MaxPooling2D新增二維的MaxPooling,設定感應範圍為2x2,每次向右或向下移動2格。
第4行:使用Conv2D新增二維的卷積層,使用3x3的卷積核(Kernel) ,使用16個卷積核,設定strides為1,設定激勵函式為relu。
第5行:使用MaxPooling2D新增二維的MaxPooling,設定感應範圍為2x2,每次向右或向下移動2格。
第6行:使用Flatten新增攤平層。
第7行:使用Dense新增全連接層,有10個神經元,使用softmax為激勵函式。
第8行:設定優化器為adam,設定損失函式為categorical_crossentropy,表示此模型為多類別分類,設定metrics為accuracy,表示訓練過程要紀錄每次訓練的正確率。
第9行:使用函式summary輸出整個模型的摘要。
Step4)訓練模型與評估模型
輸入訓練資料訓練模型,輸入測試資料評估模型。
程式說明
第1行:使用train_X為訓練資料集,train_y為訓練目標集,設定validation_data為(test_X, test_y),表示使用test_X為測試資料集,test_y為測試目標集。所有輸入資料需要跑10次,每200筆輸入資料更新一次參數,卷積層參數為卷積核內的元素值。設定verbose為2,表示每個epochs輸出一個訓練紀錄。使用變數history接收訓練過程。
第2到3行:使用evaluate輸入測試資料test_X與test_y2評估模型的正確率,將評估結果指定給變數score,顯示變數score的第二個元素,第二個元素為正確率。
專有名詞介紹
(a) epoch:跑完所有訓練資料一次,稱作1個epoch,通常機器學習模型需要反覆訓練多次,看能不能降低損失值,提高正確率。
(b) batch_size:每隔batch_size資料量更新一次參數。假設設定batch_size為200,輸入訓練資料量為10000,則跑完所有訓練資料一次,則會更新參數50(10000/200=50)次。
(c) loss:訓練資料的損失值。
(d) val_loss:測試資料的損失值。
(e) accuracy:訓練資料的正確率。
(f) val_accuracy:測試資料的正確率。
當模組學習效果不明顯,可以使用EarlyStopping提早結束訓練,程式碼如下。
from tensorflow.keras.callbacks import EarlyStopping
early = EarlyStopping(monitor='val_loss', patience=3, min_delta=0.01)
model = Sequential()
…
history = model.fit(…, epochs=20, verbose=1, callbacks=[early])
「monitor='val_loss'」表示監控測試資料的損失值,「patience=3, min_delta=0.01」表示當連續3個epochs內損失值的減少小於0.01就會結束訓練。使用「model.fit(… , callbacks=[early])」將EarlyStopping功能加入到訓練過程。
Step5)繪製模型正確率圖與損失值圖
繪製訓練過程的模型正確率圖與損失值圖。
程式說明
第1行:取出訓練過程的訓練資料的正確率,使用plot繪製到圖表上。
第2行:取出訓練過程的測試資料的正確率,使用plot繪製到圖表上。
第3到5行:設定圖表標題為「模組正確率」,Y軸標籤為「正確率」,X軸標籤為「Epoch」。
第6行:在左上角設定圖說。
第7行:繪製模組正確率圖。
第8行:取出訓練過程的訓練資料的損失值,使用plot繪製到圖表上。
第9行:取出訓練過程的測試資料的損失值,使用plot繪製到圖表上。
第10到12行:設定圖表標題為「模組損失」,Y軸標籤為「損失」,X軸標籤為「Epoch」。
第13行:在左上角設定圖說。
第14行:繪製模型損失值圖。
Step6)顯示圖片與模型預測結果
顯示手寫數字圖片、原本數字與模型預測的數字。
程式說明
第1行:自訂函式draw_digit用於顯示手寫數字圖片,並標示原始數字與辨識後數字。
第2行:使用for迴圈執行跑5次,每次顯示一個手寫數字圖片。
第3行:使用函式gcf新增一個圖片。
第4行:設定圖片大小為寬12英寸高1英寸。
第5行:使用subplot劃分成1列5欄,每張圖片放在編號i+1的位置,表示由左到右依序擺放。
第6行:使用imshow顯示圖片,將輸入手寫數字陣列digits第index+i個元素,轉換維度為28x28x1,設定cmap為binary。
第7到8行:設定標題為原始數字與辨識後的數字
第9行:設定標題字體大小為10。
第10到11行:不顯示X軸刻度與Y軸刻度。
第12行:使用迴圈新增一列五張圖片後,函式show顯示五個手寫數字在螢幕上。
第13行:使用函式predict進行模型預測,以test_X為輸入,結果儲存到變數pred_y。
第14行:此時變數pred_y為one-hot編碼,使用函式argmax輸入變數pred_y還原回數值。
第15到16行:使用for迴圈呼叫函式draw_digit顯示前25個手寫數字到螢幕上。
Step7) 儲存模型與載入模型
使用函式save儲存訓練好的模型,函式load_model載入模型。
程式說明
第1行:使用函式save儲存模型到資料夾minst_model.cnn。
第2行:使用函式load_model載入資料夾minst_model.cnn到模型。
Step8) 建立混淆矩陣
使用混淆矩陣找出辨識錯誤的數字與個數。
程式說明
第1行:使用函式predict進行模型預測,以test_X為輸入,變數結果儲存到變數pred_y
第2行:此時變數pred_y為one-hot編碼,使用函式argmax輸入變數pred_y還原回原來數字。
第3行:使用函式crosstab比較目標結果test_y與預測結果pred_y,產生混淆矩陣。
Step9)顯示辨識錯誤的手寫數字
找出辨識錯誤的手寫數字,並顯示在螢幕上。
程式說明
第1行:比較test_y與pred_y的不同之處,產生布林陣列到陣列diff,True表示兩數字不同,False表示兩數字相同。
第2行:顯示「辨識錯誤個數」,加總陣列diff的數值。
第3行:找出陣列diff元素為True的索引值到陣列diff_idx。
第4行:印出陣列diff_idx。
第5到6行:使用for迴圈取出陣列diff_idx前10個元素為索引值,使用Step6的函式draw_digit顯示該索引值開始的五張圖片,只有第一張圖片是辨識錯誤的圖片。
Step10)顯示每一層的設定
顯示模型的每一層名稱與卷積核個數,與每一層的詳細設定。
程式說明
第1行:使用函式load_model載入模型minst_model.cnn。
第2到8行:使用for迴圈找出卷積神經網路的每一層。
第3行:印出分隔線。
第4到7行:若該層的名字有出現「conv」,則顯示該層名稱與卷積核個數,否則只顯示名稱。
第8行:顯示該層的設定。
Step11)顯示卷積神經網路的每一層輸出
顯示卷積神經網路的每一層輸出。
程式說明
第1到4行:匯入函式庫。
第5行:使用函式gcf新增一個圖片。
第6行:設定圖片大小為寬2英寸高2英寸。
第7行:使用subplot劃分成1列1欄,圖片放在編號1的位置。
第8行:使用imshow顯示圖片,將輸入手寫數字陣列train_X第1個元素,設定cmap為binary。
第9行:顯示圖片到螢幕上。
第10行:使用函式load_model載入模型minst_model.cnn。
第11行:設定outputs為model.layers[0].output,輸出卷積神經網路的第一層。
第12行:使用函式summary顯示模型的摘要。
第13行:將train_X[0]的維度轉換成(1,28,28,1),變數img參考到此結果。
第14行:使用函式predict輸入變數img進行預測,變數pred_img參考到此結果。
第15行:設定變數c為1。
第16行:設定圖片大小為寬10英寸高8英寸。
第17到23行:使用巢狀迴圈顯示16個卷積核運算結果,使用函式subplot分割圖片為4列4欄,將圖片放在第c個位置(第19行),不顯示X軸刻度與Y軸刻度(第20到21行),顯示變數pred_img第c-1個元素(第22行),變數c遞增1(第23行)。
第24行:顯示圖片。