# [ML] MNIST 手寫數字辨識
###### tags: `ML`
## 簡介
MNIST手寫數字辨識是一個經典的機器學習項目,可以讓你學習分類器的基本原理。MNIST 是早在 1998 年就釋出的手寫數字辨識 dataset。因為他資料量小、架構簡單就能訓練,因此被視為深度學習界的 hello world 專案。今天我們就來用PyTorch來實作一個辨識手寫數字的模型。
今天我們選用的是最簡單的全連結神經網路,並在每一層加上 ReLU 激勵函數(activation function)。
## 實作
### Import package
在最開始我們必須import需要的package:
```python=
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as data
import torchvision.datasets as datasets
import torchvision.transforms as transforms
```
### 載入資料
我們使用`torchvision`提供的功能來下載MNIST資料:
```python=
# 訓練資料集,用於訓練模型
train_data = datasets.MNIST(
root="./mnist",
train=True,
transform=transforms.ToTensor(),
download=True
)
# 測試資料集,用於評估模型表現
test_data = datasets.MNIST(
root="./mnist",
train=False,
transform=transforms.ToTensor(),
download=True
)
```
在這邊可以先用`matplotlib`來偷看一下資料的圖片長怎樣:
```python=
import matplotlib.pyplot as plt
plt.imshow(
train_data.train_data[13].numpy(),
cmap="gray"
)
plt.title(train_data.train_labels[13].item())
```
我們進一步將訓練集分成真正的訓練集和驗證集,只有真正的訓練集用於訓練模型,驗證集用於挑選模型:
```python=
# 將數據集分成訓練集和驗證集
num_train = len(train_data)
indices = list(range(num_train))
split = int(num_train * 0.8)
train_idx, valid_idx = indices[:split], indices[split:]
train_sampler = data.SubsetRandomSampler(train_idx)
valid_sampler = data.SubsetRandomSampler(valid_idx)
```
### 定義模型
訓練之前必須先定義我們的模型。在這裡包含了三個步驟:
* 繼承`nn.Module`類別
* 覆寫`__init__()`方法
* 覆寫`forward()`方法
```python=
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.fc1 = nn.Linear(28 * 28, 128)
self.fc2 = nn.Linear(128, 128)
self.fc3 = nn.Linear(128, 10)
def forward(self, x):
x = x.view(-1, 28 * 28)
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
x = self.fc3(x)
return x
```
這個模型包含3個全連接層,分別為:
* 第1層:輸入層,有 28 * 28 個神經元,對應輸入圖像的每個像素。
* 第2層:隱藏層,有 128 個神經元,使用 ReLU activation function。
* 第3層:輸出層,有 10 個神經元,對應 10 個數字類別。
在前向傳播過程中,我們將輸入圖像的像素展平成一個向量,並通過第1層的全連接層,然後使用 ReLU activation fucntion。接下來,我們將輸出傳遞到第2層的全連接層,並再次使用 ReLU activation function。最後,我們將輸出傳遞到第3層的全連接層,並輸出預測的類別機率。
這個模型的架構非常簡單,但卻可以達到不錯的效果。如果你想要更準確的模型,可以考慮使用更複雜的模型架構,例如卷積神經網絡(CNN)。
### 訓練前的準備
在訓練之前我們先來定義幾個超參數:
```pytho=
batch_size = 128
learning_rate = 0.01
num_epochs = 7
```
這三個超參數是指在訓練深度學習模型時需要人為設定的參數:
* `batch_size`:指每個批次包含的樣本數量。如果`batch_size`設定的太小,那麼每次更新權重時,模型就會基於少量的樣本進行更新,這可能會導致訓練效率降低。反之,如果`batch_size`設定的太大,那麼每次更新權重時,模型就會基於大量的樣本進行更新,這可能會導致訓練過程變得較為不穩定。
* `learning_rate`:指用於更新模型權重的學習率。`learning_rate`設定的適當值對於模型的訓練效果非常重要。如果`learning_rate`設定的太大,那麼模型可能會跳過最佳解,而造成訓練效果更差。反之,如果`learning_rate`設定的太小,那麼訓練過程可能會非常緩慢。有時候我們也會採取一些非固定的 learning rate 調整策略,例如隨著訓練進行使得學習率越來越小等(但在本次專案中我們並沒有這麼做)。

* `num_epochs`:指訓練迭代的輪數。通常情況下,模型的訓練效果隨著訓練輪數的增加而提升,但是如果訓練輪數過多,那麼模型就有可能會出現過擬合的現象。因此,你需要在訓練過程中觀察模型的表現,並在必要時調整訓練輪數。
接著我們可以根據剛剛給定的 batch size,利用 PyTorch 的`DataLoader`轉換成批量資料:
```python=
# 將數據集轉換成批量資料
train_loader = data.DataLoader(
dataset=train_data,
batch_size=batch_size,
sampler=train_sampler
)
valid_loader = data.DataLoader(
dataset=train_data,
batch_size=batch_size,
sampler=valid_sampler
)
test_loader = data.DataLoader(
dataset=test_data,
batch_size=batch_size,
shuffle=False
)
```
然後再建立一個模型的實例、並定義好損失函數和優化器:
```python=
# 建立模型實例
model = Net()
# 定義損失函數和優化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=learning_rate)
```
在這個分類任務中,我們使用交叉熵(cross entropy)損失函數來定義模型的損失。交叉熵損失函數是一種常用的**分類**任務損失函數,它可以衡量預測的分佈和真實的分佈之間的差距。較小的交叉熵損失值表示模型的預測結果更準確,反之亦然。在我們的程式碼中使用到PyTorch的`nn.CrossEntropyLoss`函數來定義交叉熵損失。這個函數會自動計算交叉熵損失和每個類別的平均損失,所以你不必自己手動計算。
### 訓練
```python=
# 訓練模型
total_step = len(train_loader)
for epoch in range(num_epochs):
for i, (images, labels) in enumerate(train_loader):
outputs = model(images)
loss = criterion(outputs, labels)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (i+1) % 100 == 0:
print ("Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}"
.format(epoch+1, num_epochs, i+1, total_step, loss.item()))
# 評估模型
with torch.no_grad():
correct = 0
total = 0
for images, labels in valid_loader:
outputs = model(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print("Accuracy of the model on the valid images: {} %"
.format(100 * correct / total))
```