介紹如何使用 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 資料集範例圖片的縮圖。

載入資料
PyTorch 中資料的載入都是透過 torch.utils.data.Dataset 與 torch.utils.data.DataLoader 來進行的,Dataset 用於儲存資料以及標註資訊,而 DataLoader 則是用於將 Dataset 包裝成 iterable,以便用於模型訓練。
PyTorch 針對不同領域提供了不同的函式庫,例如 TorchText、TorchVision 與 TorchAudio,這些函式庫中也都包含了一些常用的資料集,在 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 中都有 transform 與 target_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() 將模型設定為驗證模式。兩種模式主要的差異在於 Dropout 與 BatchNorm 的運作方式,在不同模式下會有不同的運作方式。
整個訓練模型的過程會包含好幾個完整疊代(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 的遷移式學習並沒有獲得比較好的效果。
參考資料
- PyTorch:Quickstart
- Kaggle:Image classification workflow with fast.ai
- Kaggle:Image classification w/ fastai [fashion MNIST]
- Medium:Image classification with Fastai
- Medium:Image classification using fastai — Part 1
- UXAI:Fastai Lesson 1:Image Classification (上篇)
- fastai:Computer vision
- GitHub:ResNet Pytorch implementation for MNIST classification
