Skip to content

[PyTorch] Getting Start: 訓練分類器 —— MNIST

Last Updated on 2021-04-21 by Clay

使用一個簡單的 Toy Dataset 來訓練一個『分類器』可說是再簡單不過的 Deep Learning 練習。

今天,我要來練習使用 MNIST 手寫數字辨識資料集來使用 PyTorch 搭建一個簡單的分類器。

這次的模型比之前的 CNN 簡單,是只使用 fully connected layer (全連接層) 就完成的簡單模型。

順帶一提,我並沒有發現官方 Tutorial 裡面有關於 MNIST 的教學,因此只好直接貼官方的 Tutorial,若大家有興趣,也可以在裡頭找找自己想要看的內容:https://pytorch.org/tutorials/


模型參數

如果不清楚 MNIST 是什麼,也許可以參照我《使用 CNN 進行 MNIST 的手寫數字辨識 —— by Keras (實戰篇)》當中的說明。

那麼以下,開始簡單講解我的程式碼。完整代碼會放在文章最後頭。

import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data as dset
from torchvision import datasets, transforms


匯入必要的套件。

# GPU
device = 'cuda:0' if torch.cuda.is_available() else 'cpu'
print('GPU State:', device)


確認 GPU 是否可用,如果沒有 GPU,以 CPU 來進行運算。

# Transform
transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5,), (0.5,)),]
)

PyTorch 當中的 torchvision 有著名叫 transforms 的模組,可以將多個 transform 的 function 組合成一個 List 的資料型態。主要是用於圖片的轉換方面。

transformsToTensor(): 會將值在 [0-255] 的 PIL.Image 轉換成 (C, H, W)。為什麼不是 Numpy 常見的 HWC 排序呢?看過一直種說法是因為卷積做起來比較快。

transformsNormalize(): 需要先轉成 Tensor 型態,然後將其正規化。

# Data
trainSet = datasets.MNIST(root='MNIST', download=True, train=True, transform=transform)
testSet = datasets.MNIST(root='MNIST', download=True, train=False, transform=transform)
trainLoader = dset.DataLoader(trainSet, batch_size=64, shuffle=True)
testLoader = dset.DataLoader(testSet, batch_size=64, shuffle=False)

這裡我們準備好我們的資料。若是在當前資料夾底下沒有看到 "MNIST" 資料夾,程式便會自動從 PyTorch 內的 datasets 下載並創建 "MNIST"。

# Model
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.main = nn.Sequential(
            nn.Linear(in_features=784, out_features=128),
            nn.ReLU(),
            nn.Linear(in_features=128, out_features=64),
            nn.ReLU(),
            nn.Linear(in_features=64, out_features=10),
            nn.LogSoftmax(dim=1)
        )

    def forward(self, input):
        return self.main(input)


net = Net().to(device)
print(net)


Output:

Net(
  (main): Sequential(
    (0): Linear(in_features=784, out_features=128, bias=True)
    (1): ReLU()
    (2): Linear(in_features=128, out_features=64, bias=True)
    (3): ReLU()
    (4): Linear(in_features=64, out_features=10, bias=True)
    (5): LogSoftmax()
  )
)

以上這便是我們模型的架構,首先建立『全連接層』,784 個像素輸入,輸出 128 個神經元,經過激活函數 ReLU() (max(0, x) 的映射函數),連接到下一層 ...... 最後,由於我們要輸出 10 個標籤的分類,也就是 10 種數字 [0-9],我們最後輸出的是 10 個數值,最後再接 LogSoftmx()

這裡我只知道 Softmax 多用於多分類的預測,而 LogSoftmax 是它的改良。詳細的原理恕我孤陋學淺 ...... 以後會再發一篇補上。

forward() 這個 function 是所謂的 forward propagation 的過程,在這裡我直接呼叫了 self.main(),這也是滿多人會選擇的作法。

# Parameters
epochs = 3
lr = 0.002
criterion = nn.NLLLoss()
optimizer = optim.SGD(net.parameters(), lr=0.002, momentum=0.9)

這裡是參數設定:
epochs: 訓練的迭代次數
lr: Learning rate 的縮寫,意味著我們反向傳播的學習率
criterion: 我們使用的 Loss function
optimizer: 我們使用的優化器, momentum 為同方向的梯度更新幅度越來越大,反方向的則變小,通常都設 0.9。受限於學習時間,我也來不及測試其他大小。

# Train
for epoch in range(epochs):
    running_loss = 0.0

    for times, data in enumerate(trainLoader):
        inputs, labels = data[0].to(device), data[1].to(device)
        inputs = inputs.view(inputs.shape[0], -1)

        # Zero the parameter gradients
        optimizer.zero_grad()

        # Foward + backward + optimize
        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # Print statistics
        running_loss += loss.item()
        if times % 100 == 99 or times+1 == len(trainLoader):
            print('[%d/%d, %d/%d] loss: %.3f' % (epoch+1, epochs, times+1, len(trainLoader), running_loss/2000))

print('Training Finished.')


這裡便是我們開始訓練模型的地方。值得一提的是由於我們全連接層的第一層需要輸入 784 維(跟我 Keras 那邊的程式相同的輸入),故我們需要使用 view() 將 inputs 的維度壓到符合模型的輸入。

# Test
correct = 0
total = 0

with torch.no_grad():
    for data in testLoader:
        inputs, labels = data
        inputs, labels = inputs.to(device), labels.to(device)
        inputs = inputs.view(inputs.shape[0], -1)

        outputs = net(inputs)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print('Accuracy of the network on the 10000 test images: %d %%' % (100*correct / total))

class_correct = [0 for i in range(10)]
class_total = [0 for i in range(10)]

with torch.no_grad():
    for data in testLoader:
        inputs, labels = data[0].to(device), data[1].to(device)
        inputs = inputs.view(inputs.shape[0], -1)

        outputs = net(inputs)
        _, predicted = torch.max(outputs, 1)
        c = (predicted == labels).squeeze()
        for i in range(10):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1
            print(class_correct)
            print(class_total)

for i in range(10):
    print('Accuracy of %d: %3f' % (i, (class_correct[i]/class_total[i])))

Output:

Accuracy of the network on the 10000 test images: 92 %
Accuracy of 0: 1.000000
Accuracy of 1: 0.972603
Accuracy of 2: 0.865672
Accuracy of 3: 0.857143
Accuracy of 4: 0.941176
Accuracy of 5: 0.875000
Accuracy of 6: 0.892857
Accuracy of 7: 0.898305
Accuracy of 8: 0.950000
Accuracy of 9: 0.896104

後記

深度學習當中,還有許多我所不明瞭的部份。越是學習,就越是會發現這種事情。

昭和年代的圍棋大師木谷實老師也說過:『越極而越遠』這樣的話。意味著越是探究一個學問,就離那個學問的真理越是遙遠。

圍棋是這樣、深度學習也是這樣。

我想我會繼續把我學習的心得筆記記錄下來的。


Read More

Leave a Reply