# Lesson 13 | 神經網路概論2
## 第一節:圖像辨識基礎(1)
* 大家看一下下面這些圖像,你是否發覺你能瞬間辨認這些圖像分別代表0-9之中哪一個數字?

* 人工智慧的核心原理就是找出一個預測函數f(x),而對於手寫數字辨識而言,y是一個10類別的多類別分類,那你是否有辦法想像x的樣子呢?
## 第一節:圖像辨識基礎(2)
* 讓我們利用MNIST的手寫數字資料進行神經網路的實作吧!
* 請在[這裡](https://linchin.ndmctsgh.edu.tw/data/MNIST.csv)下載[MNIST](https://www.kaggle.com/c/digit-recognizer)的手寫數字資料。
* 一個28×28的黑白圖片的其實可以被表示成784個介於0至255的數字,這樣我們就又能把問題轉換為單純的預測問題了。
* 這邊的範例包含了訓練集以及測試集的準備。
```python=
import random
import numpy as np
import pandas as pd
data = pd.read_csv('MNIST.csv', header = None)
# Split Dataset
train_idx = np.array(random.sample(range(42000), 35000))
train_data = data[np.isin(range(42000), train_idx) == True]
train_x = train_data[list(range(1, 785))]
train_y = train_data[0]
test_data = data[np.isin(range(42000), train_idx) == False]
test_x = test_data[list(range(1, 785))]
test_y = test_data[0]
```
* 視覺化呈現部分資料。
```python=
import matplotlib.pyplot as plt
# Show image
sub_x_plot = train_x.to_numpy()
sub_x_plot = sub_x_plot.reshape((35000, 28, 28), order='F')
fig, ax = plt.subplots(2, 2)
ax[0, 0].imshow(sub_x_plot[7, :, :])
ax[0, 0].text(0, 2.5, train_y.tolist()[7], c="white")
ax[0, 1].imshow(sub_x_plot[126, :, :])
ax[0, 1].text(0, 2.5, train_y.tolist()[126], c="white")
ax[1, 0].imshow(sub_x_plot[247, :, :])
ax[1, 0].text(0, 2.5, train_y.tolist()[247], c="white")
ax[1, 1].imshow(sub_x_plot[871, :, :])
ax[1, 1].text(0, 2.5, train_y.tolist()[871], c="white")
```
## 第一節:圖像辨識基礎(3)
* 圖像資料由於數據特性的問題,將會導致資料量大上很多,尤其在未來我們很可能會面對到百萬級別的ImageNet資料集。
* 這個時候我們會面對到一個硬體的問題,那就是我們不可能預先把所有檔案都讀到RAM內。比較好的解決方法是每次訓練時只讀取小批量的訓練樣本,這樣就能有效降低記憶體的使用。
* 我們先把Training/Test的資料寫出:
```python=
pd.DataFrame(train_data).to_csv("train_data.csv", index = None)
pd.DataFrame(test_data).to_csv("test_data.csv", index = None)
```
* 在Pytorch裡面可以這樣撰寫Dataset(註:這邊的範例依舊把圖讀進記憶體,但你應該能看出如何更換成每筆資料個別讀取):
* Windows下cmd的安裝指令
```
pip3 install torch torchvision torchaudio
```
```python=
import os
import pandas as pd
from torch.utils.data import Dataset
from torchvision.io import read_image
class CustomImageDataset(Dataset):
def __init__(self, file):
self.img_labels = pd.read_csv(file, header = None)
def __len__(self):
return len(self.img_labels)
def __getitem__(self, idx):
# img_path = os.path.join(self.img_labels.iloc[idx, 0])
# image = read_image(img_path)
image = self.img_labels.iloc[idx, list(range(1, 785))]
label = self.img_labels.iloc[idx, 0]
image = np.array(image, dtype = 'float32').reshape(-1, 28, 28, order='F')
label = np.array(label)
return image, label
training_data = CustomImageDataset("train_data.csv")
```
* 這個Dataloader和之前的並沒有甚麼太大的不同,同樣是透過一樣的方式產生Data:
```python=
from torch.utils.data import DataLoader
train_dataloader = DataLoader(training_data, batch_size=32, shuffle=True)
```
## 第一節:圖像辨識基礎(4)
* 可以透過這樣的方式去取出樣本:
```python=
train_features, train_labels = next(iter(train_dataloader))
```
* 試著視覺化得到看x跟y是否正確:
```python=
import matplotlib.pyplot as plt
# Show image
sub_x_plot = np.array(train_features)
sub_x_plot = sub_x_plot.reshape((32, 28, 28), order='F')
fig, ax = plt.subplots(2, 2)
ax[0, 0].imshow(sub_x_plot[0, :, :])
ax[0, 0].text(0, 2.5, train_labels.tolist()[0], c="white")
ax[0, 1].imshow(sub_x_plot[1, :, :])
ax[0, 1].text(0, 2.5, train_labels.tolist()[1], c="white")
ax[1, 0].imshow(sub_x_plot[2, :, :])
ax[1, 0].text(0, 2.5, train_labels.tolist()[2], c="white")
ax[1, 1].imshow(sub_x_plot[3, :, :])
ax[1, 1].text(0, 2.5, train_labels.tolist()[3], c="white")
```
* 如果你願意的話,你可以更改```CustomImageDataset```,在需要的時候再讀取jpeg檔案。
## 第二節:卷積神經網路介紹(1)
* 神經網路最初的構想是利用電腦模擬大腦運作的過程,而在多層感知器中的「全連接層」所模擬的是腦細胞傳遞訊息的思考過程。
* 但回到我們的手寫數字分類問題,當我們看到這些手寫數字時,我們一眼就能認出他們了,但從「圖片」到「概念」的過程真的這麼簡單嗎?
* 現在我們面對的是視覺問題,看來除了模擬大腦思考運作的過程之外,我們還需要模擬眼睛的作用!
* 1962年時David H. Hubel與Torsten Wiesel共同發表了一篇研究:[Receptive fields, binocular interaction and functional architecture in the cat’s visual cortex](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1359523/pdf/jphysiol01247-0121.pdf),這篇研究旨在探討生物視覺系統的運作方式,並獲得了1981年的Nobel prize

* 他們的研究發現,貓咪在受到不同形狀的圖像刺激時,感受野的腦部細胞會產生不同反應

* 這樣的研究給了我們什麼啟示呢?也就是在最開始接觸光影訊號的腦神經細胞,每個細胞會辨認一種特定的簡單特徵,而且這些簡單特徵「無論出現在視野範圍的哪裡」,他們都會被激活。
## 第二節:卷積神經網路介紹(2)
* 電腦學家受到了上面生物研究的啟發,因此在多層感知器(網路中的全連接層)之前增加了一個構造,叫做「卷積器」(Convolution filter)
* 卷積器模擬了感受野最初的細胞,他們負責用來辨認特定特徵,他們的數學模式如下:

* 簡單來說,卷積器透過一個「濾鏡」對圖片進行全局的搜索,卷積後的新圖片稱為「特徵圖」。
* 「特徵圖」的意義是什麼呢?卷積器就像是最初級的視覺細胞,他們專門辨認某一種簡單特徵,那這個「特徵圖」上面數字越大的,就代表那個地方越符合該細胞所負責的特徵。

## 第二節:卷積神經網路介紹(3)
* 那假定我們有3個卷積器,分別負責「\」、「╳」、「/」,那將會產生3個特徵圖:

* 獲得特徵圖之後,還記得感知機的activated function嗎?我們為了增加神經網路的數學複雜性,會添加一些非線性函數做轉換,因此在經過卷積層後的特徵圖會再經過非線性轉換。
* 接著,由於連續卷積的特徵圖造成了訊息的重複,這時候我們經常會使用「池化層」(pooling layer)進行圖片降維,事實上他等同於把圖片的解析度調低,這同時也能節省計算量。

## 第二節:卷積神經網路介紹(4)
* 接著我們思考一下,連續的堆疊「卷積器」會產生什麼效果?

* 我們想像有一張人的圖片,假定第一個卷積器是辨認眼睛的特徵,第二個卷積器是在辨認鼻子的特徵,第三個卷積器是在辨認耳朵的特徵,第四個卷積器是在辨認手掌的特徵,第五個卷積器是在辨認手臂的特徵
* 第1.2.3張特徵圖中數值越高的地方,就分別代表眼睛、鼻子、耳朵最有可能在的位置,那將這3張特徵圖合在一起看再一次卷積,是否就能辨認出人臉的位置?
* 第4.5張特徵圖中數值越高的地方,就分別代表手掌、手臂最有可能在的位置,那將這2張特徵圖合在一起看再一次卷積,是否就能辨認出手的位置?
* 第4.5張特徵圖對人臉辨識同樣能起到作用,因為人臉不包含手掌、手臂,因此如果有個卷積器想要辨認人臉,他必須對第1.2.3張特徵圖做正向加權,而對第4.5張特徵圖做負向加權
* 所以,我們可以在神經網路中堆疊卷積器,而越淺層的卷積器所辨認的特徵越簡單,越深層的卷積器所辨認的特徵越複雜。

## 第三節:利用卷積神經網路做手寫數字辨識(1)
* 讓我們一起來實現1989年Yann LeCun所發展的世界上第一個成功的卷積神經網路(Convolutional Neural Networks,CNN),這個CNN被命名為LeNet。

## 第三節:利用卷積神經網路做手寫數字辨識(2)
* 接著讓我們來定義Model architecture:這是一個閹割版的LeNet,原版的LeNet其第一、二層的卷積器數量分別是20以及50,而第一個全連接層具有500個神經元,這個網路結構如下:
* 第一層卷積組合
* 原始圖片(28x28x1)要先經過10個5x5的「卷積器」(5x5x1x10)處理,將使圖片變成10張「一階特徵圖」(24x24x10)
* 接著這10張「一階特徵圖」(24x24x10)會經過ReLU,產生10張「轉換後的一階特徵圖」(24x24x10)
* 接著這10張「轉換後的一階特徵圖」(24x24x10)再經過2x2「池化器」(2x2)處理,將使圖片變成10張「降維後的一階特徵圖」(12x12x10)
* 第二層卷積組合
* 再將10張「降維後的一階特徵圖」(12x12x10)經過20個5x5的「卷積器」(5x5x10x20)處理,將使圖片變成20張「二階特徵圖」(8x8x20)
* 接著這20張「二階特徵圖」(8x8x20)會經過ReLU,產生20張「轉換後的二階特徵圖」(8x8x20)
* 接著這20張「轉換後的二階特徵圖」(8x8x20)再經過2x2「池化器」(2x2)處理,將使圖片變成20張「降維後的二階特徵圖」(4x4x20)
* 全連接層
* 將「降維後的二階特徵圖」(4x4x20)重新排列,壓製成「一階高級特徵」(320)
* 讓「一階高級特徵」(320)進入「隱藏層」,輸出「二階高級特徵」(150)
* 「二階高級特徵」(150)經過ReLU,輸出「轉換後的二階高級特徵」(150)
* 「轉換後的二階高級特徵」(150)進入「輸出層」,產生「原始輸出」(10)
* 「原始輸出」(10)經過Softmax函數轉換,判斷圖片是哪個類別
```python=
from torch import nn
class NeuralNetwork(nn.Module):
def __init__(self):
super().__init__()
self.main_arch = nn.Sequential(
# first conv
nn.Conv2d(in_channels = 1, out_channels = 20, kernel_size = 5),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2),
# second conv
nn.Conv2d(in_channels = 20, out_channels = 20, kernel_size = 5),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2),
# flatten
nn.Flatten(),
# first fully connected layer
nn.Linear(20 * 4 * 4, 150),
nn.ReLU(),
# first fully connected layer
nn.Linear(150, 10),
)
self.final_pred = nn.LogSoftmax(1)
def forward(self, x):
main_network = self.main_arch(x)
logits = self.final_pred(main_network)
return logits
```
* 宣告網路以及損失函數:
```python=
model = NeuralNetwork().to("cpu")
loss_fn = nn.NLLLoss()
```
## 第三節:利用卷積神經網路做手寫數字辨識(3)
* 這是我們上週使用過的函式「train」:
```python=
def train(dataloader, model, loss_fn, optimizer):
size = len(dataloader.dataset)
for batch, (X, y) in enumerate(dataloader):
X, y = X.to('cpu'), y.to('cpu')
# Compute prediction error
pred = model(X)
loss = loss_fn(pred, y)
# Backpropagation
optimizer.zero_grad()
loss.backward()
optimizer.step()
if batch % 100 == 0:
loss, current = loss.item(), (batch + 1) * len(X)
print('loss: ', round(loss, 4), '[', current, '/', size, ']')
```
* 一樣,我們必須準備優化器
```python=
import torch
optimizer = torch.optim.Adam(model.parameters(), lr = 1e-4)
```
* 開始訓練吧:
```python=
epochs = 5
model.train() # 記得要將模型宣告成訓練模式。
for t in range(epochs):
print(f"Epoch {t+1}\n-------------------------------")
train(train_dataloader, model, loss_fn, optimizer)
print("Done!")
```
## 第三節:利用卷積神經網路做手寫數字辨識(4)
* 這是我們上週使用過的函式「test」,有稍微進行修改:
```python=
def test(dataloader, model, loss_fn):
size = len(dataloader.dataset)
num_batches = len(dataloader)
model.eval() # 測試模式這個函式是重點
test_loss, correct = 0, 0
with torch.no_grad():
for X, y in dataloader:
X, y = X.to('cpu'), y.to('cpu')
pred = model(X)
test_loss += loss_fn(pred, y)
pred = torch.argmax(pred, dim=1)
correct += np.array([1 if pred[i] == y[i] else 0 for i in range(pred.shape[0])]).sum()
test_loss /= num_batches
correct /= size
print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")
```
* 讓我們讀取測試集的資料看看效果:
```python=
testing_data = CustomImageDataset("test_data.csv")
test_dataloader = DataLoader(testing_data, batch_size=32, shuffle=False)
test(test_dataloader, model, loss_fn)
```
* 你是否覺得卷積神經網路非常厲害!
## 總結:
* 為什麼卷積神經網路在圖像辨識上相較於多層感知器這麼強大呢?卷積網路最大的賣點就是卷積層,讓我們仔細思考一下他的厲害之處:
* 他會考慮各像素之間真實的相關性,而非像多層感知器一樣把每個像素視為完全獨立的特徵。這一點可以參考人類的視覺系統,我想你應該能認同你的眼睛具有平移不變性(shift invariance)的特色,因此CNN因為完美的模仿了視覺系統造就了在測試集中的高準確性。
* CNN由於權重共享的特性,導致相當的節省參數量。從過擬合的角度思考,這暗示著我們可以用較小的參數量完成複雜的網路,因此較不容易過擬合;從參數量固定的狀況下思考,CNN可以拼湊出較為複雜的結構。因此CNN的結構在影像辨識上具有相當強大的優勢!
* 回顧一下我們的課程,並檢視一下自己的學習效果。你對Python應該有了一定的熟悉度,在醫學可能的實務程式應用上,你或許還會遇到一些困難,但基於本堂課的基礎,同學應該已有能力查詢相關資料並解決,在應用方面,對於基本的數據分析,資料視覺化,甚至到演算法模型的建立,都有了一定的概念。在人工智慧的部分,儘管還有一些任務我們沒有帶各位實現,但你應該能想像到「看圖說話」、「聊天對答」等是怎樣做到的了吧?