# 使用卷積神經網絡(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的 #實作班成績上傳