# 使用卷積神經網絡(CNN)進行手寫數字辨識 ### 第六堂社課 --- ## 卷積 >一個一個做積運算 ![螢幕擷取畫面 2023-12-22 231924](https://hackmd.io/_uploads/HkDGYm7Dp.png =80%x) 卷積的出現是為了處理訊號,例如要用兩個函數合成出一個新的函數(如上圖),我們就可以用卷積擷取出函數(訊號)特徵 卷積公式: :::info $$(f(\tau)\ast g(\tau))_n=\int_0^t f(\tau)g(t-\tau)d\tau$$ ::: 卷積公式(離散): :::info $$(a\ast b)_n=\sum\limits_{i,j\ |\ i+j=n+1}a_i\cdot b_j$$ ::: :::success 意思是有$a,b$兩個陣列,其卷積後的第n項(1-based)會是各個$a_i{和}b_j{的乘積和,其中i+j=n+1}$ ::: 假設有兩個一維陣列做卷積: $[1,2,3]\ast[4,5,6]$ 先把其中一個陣列反轉 然後依序移動做對應項的乘積和 ![IMG_0711](https://hackmd.io/_uploads/HkSXL4QPp.jpg =40%x) 目前答案: [**1*4**, , , , ] ![IMG_0712](https://hackmd.io/_uploads/SyxnLEmPT.jpg =32%x) 目前答案: [4, **1\*5+2\*4**, , , ] ![IMG_0713](https://hackmd.io/_uploads/Hy1gwNmw6.jpg =25%x) 目前答案: [4, 13, **1\*6+2\*5+3\*4**, , ] ![IMG_0714](https://hackmd.io/_uploads/SyMBPVXDp.jpg =32%x) 目前答案: [4, 13, 28, **2\*6+3\*5**, ] ![IMG_0715](https://hackmd.io/_uploads/ry_wDN7wa.jpg =40%x) 目前答案: [4, 13, 28, 27, **3\*6**] 所以最後得出$[1,2,3]\ast[4,5,6]=[4, 13, 28, 27, 18]$ 還有另一個做法: 把陣列放到表格中紀錄乘積 | Conv | 1 | 2 | 3 | |:---------:|:---:|:---:|:---:| | <strong>4 | 1*4 | 2*4 | 3*4 | | <strong>5 | 1*5 | 2*5 | 3*5 | | <strong>6 | 1*6 | 2*6 | 3*6 | 然後我們沿左下-右上的對角線分別相加就可以得到結果 Ans=[1\*4, 1\*5+2\*4, 1\*6+2\*5+3\*4, 2\*6+3\*5, 3\*6]=[4, 13, 28, 27, 18] 補充資料: 為什麼卷積叫卷積: https://www.zhihu.com/question/54677157 很好的影片: https://youtu.be/KuXjwB4LzSA --- ## 圖卷積 ### 圖的數值化 ![螢幕擷取畫面 2023-12-23 144601](https://hackmd.io/_uploads/B1QUMZ4v6.png =70%x) 我們可以將每個像素以數值化表示,上圖是示意圖,1是黑0是白,每個像素是0~1之間的灰階值。 但其實大部分會在像素中有RGB三個值,值域在0~255(2^8)。 因為圖被數值化了,所以我們可以對圖做二維卷積。 ### 二維卷積 ![螢幕擷取畫面 2023-12-23 161649](https://hackmd.io/_uploads/rkUPtfED6.png =70%x) 在卷積前,我們要先決定出卷積核(kernel,又稱為filter weight)的大小和值, 然後將kernel放在圖上, 計算對應格子的乘積。 接著像sliding window那樣將kernel滑過去,再計算 重複至整張圖都被跑過為止 ![20170516153849928](https://hackmd.io/_uploads/SkosPamPp.png =70%x) ### Padding 從上面的例子可以看出在卷積後圖像大小會小一圈, 所以我們在卷積前可以將原始圖像加上一圈0(或其他值), 確保在運算後圖像大小會相同。 這就是padding ![螢幕擷取畫面 2023-12-23 104050](https://hackmd.io/_uploads/BJwTup7wT.png =70%x) ### 選擇kernel 我們已經知道了圖形的卷積算法, 接下來我們便思考要用卷積對圖形做什麼處理? 直接試試看各種kernel的效果 ```python= from google.colab import drive drive.mount('/content/drive') import numpy as np import cv2 from google.colab.patches import cv2_imshow kernel = np.array( #choose a kernel(2D array) ) image = cv2.imread('/content/drive/MyDrive/conv/hayaku.png') if image.shape[0] >= kernel.shape[0] and image.shape[1] >= kernel.shape[1]: convolved_image = cv2.filter2D(image, -1, kernel) cv2_imshow(image) cv2_imshow(convolved_image) cv2.waitKey(0) cv2.destroyAllWindows() else: print("Kernel's size is too small") ``` 原圖: ![e605d15861fd88bac0fbc984b370a0ff](https://hackmd.io/_uploads/rkz-OKVPp.png =98%x) 各種kernel: ![下載 (4)](https://hackmd.io/_uploads/S1ccOtVv6.png =98%x) >```python= >kernel = np.array([0]) >``` >:::info >不意外的整張變黑了,因為每個像素都被*0 >::: ![image](https://hackmd.io/_uploads/HyOZ5brv6.png =98%x) >```python= >kernel = np.array([[1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,], > [1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,], > [1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,], > [1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,], > [1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,], > [1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,], > [1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,], > [1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,], > [1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,1/81,] > ]) >``` >:::info >使用9*9的kernel,每格的值都是1/81,所以整張圖被模糊了9倍 >::: ![image](https://hackmd.io/_uploads/Sy_ST-rD6.png =98%x) >```python= >kernel = np.array([#1*81的kernel,放81個1/81]) >``` >:::info >水平向被模糊了81倍 >::: ![image](https://hackmd.io/_uploads/rkx8AZHDa.png =98%x) >```python= >kernel = np.array([#81*1的kernel,放81個1/81]) >``` >:::info >垂直向被模糊了81倍 >::: ![image](https://hackmd.io/_uploads/SJNwcGHP6.png =98%x) >```python= >kernel1 = np.array([[1,0,0,0,-1], > [1,0,0,0,-1], > [1,0,0,0,-1], > [1,0,0,0,-1], > [1,0,0,0,-1] > ]) >``` >:::info >偵測水平方向變化,得到垂直邊緣 >::: ![image](https://hackmd.io/_uploads/H16XiGBvT.png =98%x) >```python= >kernel2 = np.array([[1,1,1,1,1], > [0,0,0,0,0], > [0,0,0,0,0], > [0,0,0,0,0], > [-1,-1,-1,-1,-1] > ]) >``` >:::info >偵測垂直方向變化,得到水平邊緣 >::: ## 池化Pooling ![螢幕擷取畫面 2023-12-23 160841](https://hackmd.io/_uploads/HkvsBfVwT.png =80%x) 池化的意思是把特徵萃取出來, 像上圖就是將區塊中的最大值當作區域特徵(max-pooling), 也有其他運算方式如取平均值(mean-pooling)。 池化的目的是downsampling, 而downsampling能夠減少運算次數(因為把圖變小了)以及增加預測準度(因為把特徵萃取出來) ![image](https://hackmd.io/_uploads/BJPVII4wa.png =80%x) 例如以上這兩張圖片都是X,但對電腦來說卻是不同的, 所以我們用pooling就能專注於圖形的特徵(特徵萃取), 減少圖形位置不同所造成的干擾。 我們也可以再將圖形分割成許多部件, 分別比對部件的特徵, 這些都能增加預測準確度。 ## MNIST ![image](https://hackmd.io/_uploads/rymbmQrP6.png) MNIST 是一個有手寫數字圖片的資料集, 每筆資料都有一張28\*28\*1的圖片, 以及其label(答案), 共有70000筆資料, 其中60000筆是for training, 10000筆是for test。 ## 蓋CNN >參考https://github.com/erhwenkuo/deep-learning-with-keras-notebooks/blob/master/2.7-mnist-recognition-cnn.ipynb 架構圖: ![image](https://hackmd.io/_uploads/SJ-d5dHPT.png) ### 裝套件 ```pythin= !pip install tensorflow !pip install keras !pip install np_utils ``` ```python= import tensorflow as tf from keras.datasets import mnist from keras.utils import to_categorical import numpy as np import keras ``` ### 讀取資料 ```python= np.random.seed(10) # Read MNIST data (X_Train, y_Train), (X_Test, y_Test) = mnist.load_data() # Translation of data X_Train4D = X_Train.reshape(X_Train.shape[0], 28, 28, 1).astype('float32') X_Test4D = X_Test.reshape(X_Test.shape[0], 28, 28, 1).astype('float32') ``` reshape()做的事情是將原本的三維矩陣轉成四維矩陣, 用X_Train.shape就能看到資料的形狀是(60000, 28, 28), reshape後變成(60000, 28, 28, 1), 多出來的1叫做channel, 意思是這張圖片的顏色訊號只有一種(黑的程度), 如果是彩色圖片, channel數量就會是3, 因為圖片紀錄的是RGB。 ### 資料標準化 ```python= # Standardize feature data X_Train4D_norm = X_Train4D / 255 X_Test4D_norm = X_Test4D /255 # Label Onehot-encoding y_TrainOneHot = keras.utils.to_categorical(y_Train) y_TestOneHot = keras.utils.to_categorical(y_Test) ``` 將X轉換成0~1間的浮點數。 Y則做one-hot encoding, one-hot encoding指的是把一個值分類到其類別, 例如3做encoding, 就會變成[0, 0, 0, 1, 0, 0, 0, 0, 0, 0] ### 蓋模型 ```python= # Set your parameter activation=' ' kernel_size=(,) dropout= ``` >:::spoiler 參數配置提示 >#### activation >寫法範例 >```python= >activation='sigmoid' >``` > * hard_sigmoid > * linear > * mish > * relu > * selu > * sigmoid > * softmax > * softplus > * softsign > * swish > * tanh > >示意圖 >![1688885174323](https://hackmd.io/_uploads/rJgcTaiw6.jpg =80%x) > >#### kernel_size >kernel_size請設定為一個正整數的平方 >並且在9\*9以下 >不然可能要跑很久(8\*8就有點久了) > >寫法 >```python= >kernel_size=(3,3) #3*3的kernel >``` > >#### dropout >dropout rate請設為0~1間的浮點數 >```python= >dropout=0.5 >``` >::: ```python= from keras.models import Sequential from keras.layers import Dense,Dropout,Flatten,Conv2D,MaxPool2D # initialize the model model = Sequential() # Create CN layer 1 model.add(Conv2D( filters=16, kernel_size=kernel_size, padding='same', input_shape=(28,28,1), activation=activation, name='conv2d_1') ) # Create Max-Pool 1 model.add(MaxPool2D(pool_size=(2,2), name='max_pooling2d_1')) # Create CN layer 2 model.add(Conv2D( filters=36, kernel_size=kernel_size, padding='same', input_shape=(28,28,1), activation=activation, name='conv2d_2') ) # Create Max-Pool 2 model.add(MaxPool2D(pool_size=(2,2), name='max_pooling2d_2')) # Add Dropout layer model.add(Dropout(dropout, name='dropout_1')) model.add(Flatten(name='flatten_1')) model.add(Dense(128, activation=activation, name='dense_1')) model.add(Dropout(dropout, name='dropout_2')) model.add(Dense(10, activation='softmax', name='dense_2')) ``` Sequential()是將模型序列化(初始化), 假設整個模型是串燒, Sequential()就是產生竹籤, 後面加的layers是串在上面的料。 然後可以看到我們加的layers順序是 conv--->pool--->conv--->pool--->dropout conv和pool主要是擷取圖形特徵, 而dropout是將部分神經元殺掉(dropout), 防止特徵連接過多神經元而造成過擬合(overfitting) >:::spoiler overfitting >![image](https://hackmd.io/_uploads/HyvZ3dBDa.png =80%x) > >overfitting的意思是擬合曲線過度跟隨資料點(背答案), >造成模型泛化能力變差(背完這張考卷100分了,但考其他考卷還是很爛) >::: flatten layer是用來將數據壓平的(轉成一維陣列), 而dense layer(全連接層)是用於計算各個權重(統整訓練結果), 讓模型最後能夠預測(當作輸出層)。 ### 模型摘要 ```python= model.summary() ``` ### 訓練模型 ```python= # 定義訓練方式 model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) # 開始訓練 train_history = model.fit(x=X_Train4D_norm, y=y_TrainOneHot, validation_split=0.2, epochs=6, batch_size=300, verbose=1) ``` 這裡有很多訓練用的參數, 大家可以玩玩看。 validation_split是將訓練集分出驗證集來, 類似於監督模型的訓練。 epochs是訓練的次數。 batch_size是指每次輸入多少筆資料(所以一次的epoch要輸入約160次,160\*300=48000=60000*0.8) ### 畫出訓練結果 ```python= import matplotlib.pyplot as plt def plot_image(image): fig = plt.gcf() fig.set_size_inches(2,2) plt.imshow(image, cmap='binary') plt.show() def plot_images_labels_predict(images, labels, prediction, idx, num=10): fig = plt.gcf() fig.set_size_inches(12, 14) if num > 25: num = 25 for i in range(0, num): ax=plt.subplot(5,5, 1+i) ax.imshow(images[idx], cmap='binary') title = "l=" + str(labels[idx]) if len(prediction) > 0: title = "l={},p={}".format(str(labels[idx]), str(prediction[idx])) else: title = "l={}".format(str(labels[idx])) ax.set_title(title, fontsize=10) ax.set_xticks([]); ax.set_yticks([]) idx+=1 plt.show() def show_train_history(train_history, train, validation): plt.plot(train_history.history[train]) plt.plot(train_history.history[validation]) plt.title('Train History') plt.ylabel(train) plt.xlabel('Epoch') plt.legend(['train', 'validation'], loc='upper left') plt.show() ``` #### accuracy ```python= show_train_history(train_history, 'accuracy', 'val_accuracy') ``` #### loss ```python= show_train_history(train_history, 'loss', 'val_loss') ``` ### 測試模型 ```python= print("\t[Info] Making prediction of X_Test4D_norm") prediction_x = model.predict(X_Test4D_norm) # Making prediction and save result to prediction classes_x=np.argmax(prediction_x,axis=1) print() print("\t[Info] Show 10 prediction result (From 240):") print("%s\n" % (classes_x[240:250])) ``` ```python= plot_images_labels_predict(X_Test, y_Test, classes_x, idx=100) ``` ```python= scores = model.evaluate(X_Test4D_norm, y_TestOneHot) print() print("\t[Info] Accuracy of testing data = {:2.1f}%".format(scores[1]*100.0)) ``` # 比賽 我在上面那段程式碼留了三個參數給大家調整(activation、kernel_size和dropout) 請各位自行調整並訓練模型,再將最後一欄的輸出結果截圖傳至社團discord的 #實作班成績上傳