介紹如何使用 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