Python

PyTorch 深度學習函式庫 Fashion-MNIST 影像分類入門教學與範例

介紹如何使用 PyTorch 深度學習函式庫,以 Fashion-MNIST 資料集訓練影像分類模型,並進行預測。

Fashion-MNIST 資料集

Fashion-MNIST 資料集中包含了 60,000 張圖片的訓練資料集與 10,000 張圖片的測試資料集,圖片的格式都是灰階的,類別分為以下 10 類:

類別編號 說明
0 T-shirt/top
1 Trouser
2 Pullover
3 Dress
4 Coat
5 Sandal
6 Shirt
7 Sneaker
8 Bag
9 Ankle boot

以下是一些 Fashion-MNIST 資料集範例圖片的縮圖。

Fashion-MNIST 資料集範例圖片

載入資料

PyTorch 中資料的載入都是透過 torch.utils.data.Datasettorch.utils.data.DataLoader 來進行的,Dataset 用於儲存資料以及標註資訊,而 DataLoader 則是用於將 Dataset 包裝成 iterable,以便用於模型訓練。

PyTorch 針對不同領域提供了不同的函式庫,例如 TorchTextTorchVisionTorchAudio,這些函式庫中也都包含了一些常用的資料集,在 TorchVision 函式庫中包含了許多的資料集,而 Fashion-MNIST 資料集也涵蓋於其中,所以我們可以直接從 TorchVision 函式庫載入此資料集。

import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets
from torchvision.transforms import ToTensor, Lambda, Compose
import matplotlib.pyplot as plt

# 取得 Fashion-MNIST 的訓練資料集,建立 Dataset
training_data = datasets.FashionMNIST(
    root="data",            # 資料放置路徑
    train=True,             # 訓練資料集
    download=True,          # 自動下載
    transform=ToTensor(),   # 資料轉換函數
)

# 下載 Fashion-MNIST 的測試資料集,建立 Dataset
test_data = datasets.FashionMNIST(
    root="data",            # 資料放置路徑
    train=False,            # 測試資料集
    download=True,          # 自動下載
    transform=ToTensor(),   # 資料轉換函數
)

在每一個 TorchVision 的 Dataset 中都有 transformtarget_transform 兩個參數,分別可用來指定資料與標註的轉換函數。

接著再以 Dataset 建立 DataLoader,將 Dataset 包裝成一個 iterable,提供自動批次載入(batching)、隨機取樣(sampling)、亂數排序(shuffling)、平行化載入(multiprocess data loading)功能。

這裡我們將 batch_size 設定為 64,代表 DataLoader 在疊代時每次載入 64 筆資料與標註。

# 批次載入資料筆數
batch_size = 64

# 建立 DataLoader
train_dataloader = DataLoader(training_data, batch_size=batch_size)
test_dataloader = DataLoader(test_data, batch_size=batch_size)

建立好的 DataLoader 可以透過簡單的 for 迴圈來測試資料的載入:

# 測試以 DataLoader 載入資料
for X, y in test_dataloader:
    print("Shape of X [N, C, H, W]: ", X.shape)
    print("Shape of y: ", y.shape, y.dtype)
    break
Shape of X [N, C, H, W]:  torch.Size([64, 1, 28, 28])
Shape of y:  torch.Size([64]) torch.int64

這裡的 X 就是批次載入的影像資料,其尺寸為 [64, 1, 28, 28],代表 64 張大小為 28×28 的灰階影像;y 則為 64 張影像對應的標註資訊。

建立模型

若要在 PyTorch 中建立類神經網路(neural network)的模型,可以先繼承 nn.Module 類別,然後在 __init__ 函數中建立類神經網路的各層(layers)結構,並在 forward 函數中定義資料通過類神經網路的路徑。

# 定義類神經網路模型
class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()

        # 建立類神經網路各層
        self.flatten = nn.Flatten()  # 轉為一維向量
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(28*28, 512),   # 線性轉換
            nn.ReLU(),               # ReLU 轉換
            nn.Linear(512, 512),
            nn.ReLU(),
            nn.Linear(512, 10)
        )

    def forward(self, x):
        # 定義資料如何通過類神經網路各層
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits

在 PyTorch 中我們可以透過 torch.cuda.is_available() 函數來判斷是否有 GPU 環境可以使用,若有 GPU 環境則可使用 GPU 加速運算,否則就使用普通的 CPU:

# 若 CUDA 環境可用,則使用 GPU 計算,否則使用 CPU
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using {device} device")
Using cuda device

建立類神經網路模型,並將模型放置在合適的計算設備上(GPU 或 CPU):

# 建立類神經網路模型,並放置於 GPU 或 CPU 上
model = NeuralNetwork().to(device)
print(model)
NeuralNetwork(
  (flatten): Flatten(start_dim=1, end_dim=-1)
  (linear_relu_stack): Sequential(
    (0): Linear(in_features=784, out_features=512, bias=True)
    (1): ReLU()
    (2): Linear(in_features=512, out_features=512, bias=True)
    (3): ReLU()
    (4): Linear(in_features=512, out_features=10, bias=True)
  )
)

訓練模型

若要訓練模型,必須先定義好損失函數(loss function)學習優化器(optimizer)

# 損失函數
loss_fn = nn.CrossEntropyLoss()

# 學習優化器
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)

定義訓練模型用的函數:

# 訓練模型
def train(dataloader, model, loss_fn, optimizer):
    # 資料總筆數
    size = len(dataloader.dataset)

    # 將模型設定為訓練模式
    model.train()

    # 批次讀取資料進行訓練
    for batch, (X, y) in enumerate(dataloader):
        # 將資料放置於 GPU 或 CPU
        X, y = X.to(device), y.to(device)

        pred = model(X)         # 計算預測值
        loss = loss_fn(pred, y) # 計算損失值(loss)

        optimizer.zero_grad()   # 重設參數梯度(gradient)
        loss.backward()         # 反向傳播(backpropagation)
        optimizer.step()        # 更新參數

        # 輸出訓練過程資訊
        if batch % 100 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")

定義測試模型用的函數:

# 測試模型
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:
            # 將資料放置於 GPU 或 CPU
            X, y = X.to(device), y.to(device)

            # 計算預測值
            pred = model(X)

            # 計算損失值的加總值
            test_loss += loss_fn(pred, y).item()

            # 計算預測正確數量的加總值
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()

    # 計算平均損失值與正確率
    test_loss /= num_batches
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")

在訓練模型的時候,必須呼叫 model.train() 將模型設定為訓練模式,而如果是在測試模型的時候,就要呼叫 model.eval() 將模型設定為驗證模式。兩種模式主要的差異在於 DropoutBatchNorm 的運作方式,在不同模式下會有不同的運作方式。

整個訓練模型的過程會包含好幾個完整疊代(epochs),每一個 epoch 都會呼叫我們定義的 train() 函數進行模型的訓練,然後再呼叫我們自己定義的 test() 函數進行模型準確度的驗證。

# 設定 epochs 數
epochs = 5

# 開始訓練模型
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model, loss_fn)
print("完成!")
Epoch 1
-------------------------------
loss: 2.303281  [    0/60000]
loss: 2.294579  [ 6400/60000]
loss: 2.272805  [12800/60000]
loss: 2.263779  [19200/60000]
loss: 2.251412  [25600/60000]
loss: 2.217496  [32000/60000]
loss: 2.219186  [38400/60000]
loss: 2.179404  [44800/60000]
loss: 2.169193  [51200/60000]
loss: 2.140038  [57600/60000]
Test Error: 
 Accuracy: 56.2%, Avg loss: 2.139541 

Epoch 2
-------------------------------
loss: 2.147283  [    0/60000]
loss: 2.135319  [ 6400/60000]
loss: 2.069212  [12800/60000]
loss: 2.088919  [19200/60000]
loss: 2.032584  [25600/60000]
loss: 1.967074  [32000/60000]
loss: 1.990475  [38400/60000]
loss: 1.901368  [44800/60000]
loss: 1.902262  [51200/60000]
loss: 1.820905  [57600/60000]
Test Error: 
 Accuracy: 58.7%, Avg loss: 1.834308 

Epoch 3
-------------------------------
loss: 1.869649  [    0/60000]
loss: 1.834361  [ 6400/60000]
loss: 1.710333  [12800/60000]
loss: 1.761547  [19200/60000]
loss: 1.658897  [25600/60000]
loss: 1.604551  [32000/60000]
loss: 1.631636  [38400/60000]
loss: 1.531466  [44800/60000]
loss: 1.556544  [51200/60000]
loss: 1.452586  [57600/60000]
Test Error: 
 Accuracy: 62.8%, Avg loss: 1.483442 

Epoch 4
-------------------------------
loss: 1.547206  [    0/60000]
loss: 1.517404  [ 6400/60000]
loss: 1.364653  [12800/60000]
loss: 1.449112  [19200/60000]
loss: 1.344921  [25600/60000]
loss: 1.324505  [32000/60000]
loss: 1.350635  [38400/60000]
loss: 1.268585  [44800/60000]
loss: 1.304660  [51200/60000]
loss: 1.212856  [57600/60000]
Test Error: 
 Accuracy: 64.2%, Avg loss: 1.242051 

Epoch 5
-------------------------------
loss: 1.308732  [    0/60000]
loss: 1.299642  [ 6400/60000]
loss: 1.129227  [12800/60000]
loss: 1.245768  [19200/60000]
loss: 1.132431  [25600/60000]
loss: 1.136156  [32000/60000]
loss: 1.170655  [38400/60000]
loss: 1.097915  [44800/60000]
loss: 1.142273  [51200/60000]
loss: 1.062145  [57600/60000]
Test Error: 
 Accuracy: 65.1%, Avg loss: 1.085160 

完成!

當訓練完 5 個 epochs,準確度只有 65.1%,若要提高準確度,可以增加訓練的 epochs 數,當經過 500 個 epochs 的訓練,準確度就提升到 88.2%:

Epoch 500
-------------------------------
loss: 0.150717  [    0/60000]
loss: 0.223126  [ 6400/60000]
loss: 0.148979  [12800/60000]
loss: 0.221414  [19200/60000]
loss: 0.212011  [25600/60000]
loss: 0.249443  [32000/60000]
loss: 0.223805  [38400/60000]
loss: 0.275266  [44800/60000]
loss: 0.290403  [51200/60000]
loss: 0.238688  [57600/60000]
Test Error: 
 Accuracy: 88.2%, Avg loss: 0.332794

儲存與載入模型參數

若要將訓練好的模型參數儲存下來,可以執行:

# 儲存模型參數
torch.save(model.state_dict(), "model.pth")

之後若要再度使用此模型,可以在建立好模型之後,載入模型的參數:

# 建立類神經網路模型
model2 = NeuralNetwork()

# 載入模型參數
model2.load_state_dict(torch.load("model.pth"))

預測

訓練好的模型可以用於新資料的預測:

# 各類別名稱
classes = [
    "T-shirt/top",
    "Trouser",
    "Pullover",
    "Dress",
    "Coat",
    "Sandal",
    "Shirt",
    "Sneaker",
    "Bag",
    "Ankle boot",
]

# 將模型設定為驗證模式
model2.eval()

# 取得測試資料
x, y = test_data[0][0], test_data[0][1]

with torch.no_grad(): # 不要計算參數梯度
    # 以模型進行預測
    pred = model2(x)

    # 整理測試結果
    predicted, actual = classes[pred[0].argmax(0)], classes[y]
    print(f'預測值:"{predicted}" / 實際值:"{actual}"')
預測值:"Ankle boot" / 實際值:"Ankle boot"

CNN 模型

上面的範例中我們只有採用最簡單的類神經網路結構,事實上若採用 CNN 的類神經網路結構,可以達到更高的準確率,以下是一個 CNN 類神經網路結構範例:

# 定義類神經網路模型
class FashionMNIST_CNN(nn.Module):
    def __init__(self):
        super(FashionMNIST_CNN, self).__init__()

        # 建立類神經網路各層
        self.layer1 = nn.Sequential(
            nn.Conv2d(in_channels=1, out_channels=32, kernel_size=3, padding=1),
            nn.BatchNorm2d(32),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size=2, stride=2)
        )

        self.layer2 = nn.Sequential(
            nn.Conv2d(in_channels=32, out_channels=64, kernel_size=3),
            nn.BatchNorm2d(64),
            nn.ReLU(),
            nn.MaxPool2d(2)
        )

        self.flatten = nn.Flatten()

        self.layer3 = nn.Sequential(
            nn.Linear(in_features=64*6*6, out_features=600),
            nn.Dropout2d(0.25),
            nn.ReLU(),
            nn.Linear(in_features=600, out_features=10)
        )

    def forward(self, x):
        # 定義資料如何通過類神經網路各層
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.flatten(x)
        logits = self.layer3(x)
        return logits

更換類神經網路時,只需要定義新的類神經網路類別,其餘的程式碼都不需要更動。

若採用這樣的 CNN 類神經網路結構,經過 50 個 epochs 的訓練就可以達到 90.8% 的準確率。

Epoch 50
-------------------------------
loss: 0.159268  [    0/60000]
loss: 0.225917  [ 6400/60000]
loss: 0.128645  [12800/60000]
loss: 0.216398  [19200/60000]
loss: 0.236906  [25600/60000]
loss: 0.306108  [32000/60000]
loss: 0.230174  [38400/60000]
loss: 0.276201  [44800/60000]
loss: 0.184314  [51200/60000]
loss: 0.160359  [57600/60000]
Test Error: 
 Accuracy: 90.8%, Avg loss: 0.255790

ResNet18 模型

我們也可以將 ResNet18 的模型用於 Fashion-MNIST 影像的分類,但是由於 ResNet18 模型的輸入影像格式為 RGB,而,Fashion-MNIST 原始影像格式則為灰階,所以在輸入之前要先把灰階轉為 RGB 格式:

from torchvision import models

# 定義類神經網路模型
class FashionMNIST_ResNet18(nn.Module):
    def __init__(self):
        super(FashionMNIST_ResNet18, self).__init__()

        # 載入 ResNet18 類神經網路結構
        self.model = models.resnet18(pretrained=True)

        # 鎖定 ResNet18 預訓練模型參數
        #for param in self.model.parameters():
        #    param.requires_grad = False

        # 修改輸出層輸出數量
        self.model.fc = nn.Linear(512, 10)

    def forward(self, x):
        # 將灰階影像轉為 RGB 格式
        x = x.repeat(1, 3, 1, 1)

        logits = self.model(x)
        return logits
Epoch 9
-------------------------------
loss: 0.151913  [    0/60000]
loss: 0.111155  [ 6400/60000]
loss: 0.128473  [12800/60000]
loss: 0.171799  [19200/60000]
loss: 0.143621  [25600/60000]
loss: 0.202515  [32000/60000]
loss: 0.154698  [38400/60000]
loss: 0.159781  [44800/60000]
loss: 0.177525  [51200/60000]
loss: 0.077286  [57600/60000]
Test Error: 
 Accuracy: 88.6%, Avg loss: 0.343564 

以此例來說,僅使用簡略 ResNet18 的遷移式學習並沒有獲得比較好的效果。

參考資料

Share
Published by
Office Guide
Tags: PyTorch

Recent Posts

Python 使用 PyAutoGUI 自動操作滑鼠與鍵盤

本篇介紹如何在 Python ...

9 個月 ago

Ubuntu Linux 以 WireGuard 架設 VPN 伺服器教學與範例

本篇介紹如何在 Ubuntu ...

9 個月 ago

Linux 網路設定 ip 指令用法教學與範例

本篇介紹如何在 Linux 系...

9 個月 ago

Windows 使用 TPM 虛擬智慧卡保護 SSH 金鑰教學與範例

本篇介紹如何在 Windows...

10 個月 ago

Linux 以 Shamir’s Secret Sharing 分割保存金鑰教學與範例

介紹如何在 Linux 中使用...

11 個月 ago

Linux 以 Cryptsetup、LUKS 加密 USB 隨身碟教學與範例

介紹如何在 Linux 系統中...

11 個月 ago