自己訓練 CNN 來進行 Image Classification (Pytorch)

ExcitedMail
SWF Lab
Published in
14 min readNov 28, 2022
https://huggingface.co/tasks/image-classification

丟入一張圖片,等個幾毫秒,噹!這是埃及貓。

這就是我們今天要談的 Image Classification,屬於 Deep Learning 底下做關於圖片的種類,其他關於圖片的處理還有 Object Detection, Image Captioning, GAN等等,未來如果有機會將會繼續介紹。

何為 Image Classification ?

給定一個種類的集合,丟入圖片後, Model 會告訴你這張照片屬於哪個種類,或是給出每個種類的機率大小,像是下圖這個例子, Model 認為他有 58.5 % 的機率為貓熊( 團團QQ ),較不可能為狗或貓。

https://jnyh.medium.com/using-artificial-neural-network-for-image-classification-9df3c34577dd

要介紹的 Model 稱為 CNN ,全稱為 Convolutional Neural Network ,介紹這個 Model 之前,我們先簡單說明 CNN 是怎麼 work 的。

以一個簡單的任務來說,假設我們要判斷一個寫著 2 的圖片,我們會看到他以上半圓開始,往左下拉之後再往右邊劃過去。

CNN 也是做差不多的事情,只是今天 CNN 沒辦法一次看整個圖片,他是用一個 k*k 的視窗,抓出這個視窗的特徵,把原始圖片轉成另一個 feature map ,再用下一個視窗來抓出下一階段的特徵,直到最後一個 feature map ,用一堆非線性函數來分配到各個種類的機率。

舉個例子,同樣以 2 來說, 第一層 feature map 可能抓出了不同方向的直線,下一層開始能抓出曲線的特徵,接著能抓出半圓形,最後抓出了圖片是由上半圓. 右上到左下的直線及橫的直線構成,最後判斷出這是一張寫著 2 的圖片。

下圖代表的就是大致上前面講的內容,接著我們把 convolution layer 及 fully-connected layer 抓出來說明他們的作用。

https://towardsdatascience.com/a-comprehensive-guide-to-convolutional-neural-networks-the-eli5-way-3bd2b1164a53

Convolutional Layer

Convolution layer 主要由 2 個函示組成,分別為 Conv2d 以及 MaxPool2d ,最後會生成一組 feature map ,或者也可以說 feature vector ,代表最後抓出來的特徵們。

Conv2d
作用為 2d convolution ,就是前面提到取出視窗內的特徵,這邊有幾個參數可以設定,詳細可以參考最下面的 doc。
舉幾個必要或比較常用的:in_channel 代表輸入的 feature 層數, out_channel 代表輸出的 feature 層數,以一張 RGB 彩色圖片來說是 3 ,如果你前面接的是另一個 Conv2d 的結果,則有可能會是他的 out_channel ; kernel_size 代表視窗的大小,對於該視窗會用矩陣算出一條 out_channel 長度的 feature vector ;stride 代表每算完一次 kernel 之後要移動多少格;padding 代表 feature 的最外面要貼多寬的 0 ,是為了防止最邊緣的參數被參考比較少次;dilation 代表 kernel 計算時抓的點間隔多少。
底下找個兩張圖舉例兩種參數設定:

in_channel = out_channel = 1, kernel_size = 1, stride = 1, padding = 0, dilation = 1
in_channel = out_channel = 1, kernel_size = 3, stride = 1, padding = 0, dilation = 1
in_channel = 3, out_channel = 2, kernel_size = 3, stride = 3, padding = 1, dilation = 2

MaxPool2d
以上圖第一個例子來說,如果我們今天希望 out_channel 有 16 層,代表出來的大小會變成原本的 16 倍,多次下來很佔記憶體空間,並且可能只有少數的資料是重要的。那麼這時候我們就需要縮小 feature map 的大小,MaxPool2d 的作用就在此處,取出某個視窗內最大的數字,一方面縮小 feature map 的大小,一方面保留重要的因子,如下圖範例:

https://paperswithcode.com/method/max-pooling

到此為止就是 convolutional layer 最重要的兩個函式,此外我們還會用一些方法來加強學習的效率,不過由於不是 CNN 專屬的,等底下 model architecture 的部分再來簡略說明。

Fully-Connected Layer

Fully-connected layer 其實只是負責把 feature map 轉換成各個種類的機率,由於 feature map 會是多維度的資料,我們會先壓縮資料成單一條 feature vector 以便計算,通常只是用線性計算而已。

Model Architecture

根據上面所述,以及加入一些增加效率的函式,我們可以將 model 建成下面的樣子,其中 BatchNorm2d 是對 batch 做 normalization、ReLU 是把負值變成 0 、Dropout 是停掉一部分的 neuron 、 Flatten 是把 feature map 壓縮成一維的 feature vector、Linear 是把最後的 vector 對應到 class 的數量。
至於最底下的 forward 是用來描述資料傳遞的順序,這次就依序傳入兩個 layer 就好了。

class ClassificationModel(nn.Module):
def __init__(self):
super().__init__()
self.cl = nn.Sequential(
nn.Conv2d(1, 16, kernel_size = 3, stride = 1, padding = 1),
nn.BatchNorm2d(16),
nn.ReLU(),
nn.Conv2d(16, 32, kernel_size = 3, stride = 1, padding = 1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2,2),
nn.Dropout(),

nn.Conv2d(32, 64, kernel_size = 3, stride = 1, padding = 1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 128, kernel_size = 3, stride = 1, padding = 1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2,2),
nn.Dropout(),
)
self.fc = nn.Sequential(
nn.Flatten(),
nn.Linear(6272, 10) # 10 means num_labels
)

def forward(self, xb):
feat = self.cl(xb)
return self.fc(feat)

Sample Code

寫到這邊發現真的要跑起整個 model 還有很多東西要說,比如說 dataset 怎麼使用、 cuda 是做甚麼的、 optimizer 是甚麼等等,不過這方面等下一段時間再說吧,底下會說明各個部分大概在做甚麼。如果自己的主機環境有設定好就能直接跑,或是可以丟上 colab 跑看看。

Import 一些會用到的 library

from tqdm import tqdm
import torch
import torchvision
from torchvision import transforms, datasets

from torch.utils.data.dataloader import DataLoader
from torch.utils.data import random_split
import torch.utils.data as data
import torch.nn as nn
import torch.nn.functional as F

最重要的 model 本身


# Model Architecture
class ClassificationModel(nn.Module):
def __init__(self):
super().__init__()
self.cl = nn.Sequential(
nn.Conv2d(1, 16, kernel_size = 3, stride = 1, padding = 1),
nn.BatchNorm2d(16),
nn.ReLU(),
nn.Conv2d(16, 32, kernel_size = 3, stride = 1, padding = 1),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(2,2),
nn.Dropout(),

nn.Conv2d(32, 64, kernel_size = 3, stride = 1, padding = 1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.Conv2d(64, 128, kernel_size = 3, stride = 1, padding = 1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2,2),
nn.Dropout(),
)
self.fc = nn.Sequential(
nn.Flatten(),
nn.Linear(6272, 10) # 10 means num_labels
)

def forward(self, xb):
feat = self.cl(xb)
return self.fc(feat)

計算 acc 的函式

# calculate acc
def accuracy(outputs, labels):
_, preds = torch.max(outputs, dim=1)
return torch.tensor(torch.sum(preds == labels).item() / len(preds))

偵測是否有 cuda 可以使用

# Detect if we have a GPU available
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print('Training on', device)

取得 datasets 並丟入 dataloader

# get dataset
train_data = datasets.FashionMNIST(
root="data",
train=True,
download=True,
transform=transforms.ToTensor()
)


# split to train & val
train_data_size = int(len(train_data) * 0.9)
val_data_size = len(train_data) - train_data_size
train_data, val_data = data.random_split(train_data, [train_data_size, val_data_size])

# set batch size and load dataset into dataloader
batch_size = 256

print(f"Length of Train Data : {len(train_data)}")
print(f"Length of Val Data : {len(val_data)}")

train_dl = DataLoader(train_data, batch_size, shuffle = True, num_workers = 4, pin_memory = True)
val_dl = DataLoader(val_data, batch_size*2, num_workers = 4, pin_memory = True)

建立 model 並丟入 device ( cuda or cpu) 並設定參數及 optimizer

model = ClassificationModel()
model = model.to(device)

# find best epoch, lr hyperparameter
num_epochs = 10
lr = 0.001
optimizer = torch.optim.Adam(model.parameters(), lr)

開始訓練並印出 loss 及 acc

# start training
for epoch in range(num_epochs):

# training
model.train()
for images, labels in tqdm(train_dl):
optimizer.zero_grad()
images = images.to(device)
labels = labels.to(device)
out = model(images) # Generate predictions
train_loss = F.cross_entropy(out, labels) # Calculate loss
train_acc = accuracy(out, labels)
train_loss.backward()
optimizer.step()

# validation
model.eval()
for images, labels in tqdm(val_dl):
images = images.to(device)
labels = labels.to(device)
out = model(images) # Generate predictions
val_loss = F.cross_entropy(out, labels) # Calculate loss
val_acc = accuracy(out, labels) # Calculate accuracy

print('Epoch', epoch, '\nTrain Loss', f'\t{train_loss.item():.4f}', 'Train Acc', f'\t{train_acc.item():.4f}', '\nVal Loss ', f'\t{val_loss.item():.4f}', 'Val Acc ', f'\t{val_acc.item():.4f}')

杰哥這邊訓練了 10 個 epoch 結果如下:

可以看到第一個 epoch 的表現就不錯了,後續提升的幅度也有限,其中一個原因是這次的 dataset 是 FashionMNIST ,比較簡單,不但只有黑白圖片,並且種類也只有 10 種。

後來我把 dataset 換成 CIFAR 100 ,並且加深了 model 再來做 training,可以明顯看到 model 有慢慢在變強,以下是訓練結果:

雖然結果還沒有很好,不過耐心的多試試看別的參數或是調整 model 就能變得更好呢!

雖然 CNN 算是相對簡單的 model ,但整理成一篇文章也是蠻花時間的 QQ ,有機會的話下次介紹比較有趣的 GAN 。

--

--