機器學習分類器在沈浸式環境之應用 — — 4. Predefined Model and Data Augmentation

ExcitedMail
SWF Lab
Published in
15 min readMay 24, 2023

之前寫過如何自己寫 Custom Dataset,以及怎麼建立 CNN Model,有興趣可以點連結過去看看,透過這兩篇已經能做出堪用的 Classification Model 了。

如果要做到更好的結果,通常需要花蠻多時間調整參數並測試,所以這篇文章會介紹怎麼使用別人建立好甚至訓練過的 Model 直接對自己的資料進行訓練。

小小補充一下,通常從頭開始訓練會稱為 Training,而用別人訓練好的 Model 丟入自己的資料調整成適合特定任務的行為會稱為 Fine-tuning。

感謝 Corn 以及 QQAI 幫我 Review 這篇文章!

Table of Contents

  • Intro.
  • Predefined Model
  • Data Augmentation
  • Conclusion
  • Reference

Intro.

雖然我們可以自己建 Model 並且透過調整 layer 以及參數達到不錯的準確度,不過這樣子的 Model 通常只適合當前訓練的任務。如果今天換了另一組資料需要分類,不但需要重新訓練,layer 的組合也不一定適合新的資料。

除此之外,有許多研究機器學習的學者會不斷的研究最適合各種領域的 Model 組合,例如語音辨識、字句生成以及圖形辨識等等。沒有花大把時間進行實驗以及研究,很難超越這些學者的成果。秉持打不過就加入的想法,我們可以直接抓他們組合出來的 Model 來用。

除此之外,我們自己抓資料時,很難抓到非常大量的資料,不夠多的資料導致難以成功訓練模型,所以這篇另外會介紹怎麼把少量的資料數翻倍。

Predefined Model

CNN Model 較為有名的是 Residual Network 這個架構,通常先由 2 個 CNN layer 組成一個 Residual Block,資料傳到任何 Residual Block 後有兩個行為,一個是經過該 Block 提取出重要資訊,另一個行為是跳過該 Block,增加提取資料的變化,而多個 Residual Block 組成出來的 Network 就稱為 Residual Network,最終簡稱為 ResNet,ResNet 後面接的數字就代表 CNN layer 的層數,例如 ResNet50、ResNet152 等等。

K. Chen, K. Chen, Q. Wang, Z. He, J. Hu and J. He, “Short-Term Load Forecasting With Deep Residual Networks,” in IEEE Transactions on Smart Grid, vol. 10, no. 4, pp. 3943–3952, July 2019, doi: 10.1109/TSG.2018.2844307.

以下會以 ResNet50 來做示範,他的 input image size 定為 224*224,Rescale 成 0~1 之後還要 Normalize 到 mean=[0.485, 0.456, 0.406]std=[0.229, 0.224, 0.225] ,一般來說都會在 Document 提及,要記得在 transform 的時候跟著調整,Model 的表現才會更好。

通常 Model 都會有 cnn layer output vector 的長度,以 ResNet50 來說,cnn layer output vector 的長度為 2048 ,所以我們修改 fc layer 時要記得先取得長度才能讓 Model 正常運作。

from torchvision import models
import torchvision.transfomrs as T

# 訓練的種類數量
num_classes = 10

# Transform 成符合 Model 的樣子 之後會丟入 Dataset 中運作
transform_set = T.Compose([
T.Resize(224),
T.ToTensor(),
T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

# 如果想要用調整好的參數
# model = models.resnet50(models.ResNet50_Weights.DEFAULT)
model = models.resnet50()
num_feat = model.fc.in_features # 取得 Model 的 fc layer input 長度
model.fc = nn.Linear(in_features, num_classes) # 修改 fc layer

如果想要使用別人 Train 好的 Model ,就要使用註解掉的那一行,反之如果只是要拿架構來使用,並且用自己的 Dataset 進行訓練,就用底下沒有註解掉的那行。

為了解決不夠多資料進行訓練的問題,我們需要 Data Augmentation 的幫忙。

Data Augmentation

Data Augmentation 直翻的意思是資料強化,行為上是把資料經過一些轉換,可以讓 Model 訓練的更好(像上面的例子一樣),或是可以讓資料的變化更多元(旋轉、翻轉、改變色調之類的),下面會介紹幾種 Augmentation 的方式,另外還有許多方式,可以在這裡找到。

Resize
會把圖片的解析度重新設定,通常丟入 Model 之前會用到這個或是底下兩個裁切的功能。要提供 Resize 的大小,可以使用 int 或是 tuple。

RandomCrop
從圖片中擷取隨機一塊,通常會先 Resize 之後再做這件事,並且大小不會差太多,以防圖片的重點被裁掉導致訓練效果差。不過下圖只是為了示範,沒有先做 Resize 直接做 RandomCrop。

CenterCrop
從中間擷取一塊圖片,比 RandomCrop 還要常用,通常不用擔心圖片重點被裁掉的問題。

RamdomRotation
對圖片做隨機旋轉,旋轉以及底下兩個翻轉都要考慮圖片是否適合做這個操作,比如說文字辨識就不太適合這麼處理。

RandomHorizontalFlip
對圖片做水平翻轉,和底下的垂直翻轉都只要給執行的機率即可。

RandomVerticalFlip
對圖片做垂直翻轉。

ColorJitter
調整圖片的色調,可以調整的參數相對複雜,對於某些圖片可能不太適合這麼調整,比如說把蛋黃調整成綠色的會導致 Model 難以訓練。

GaussianBlur
中文翻譯為高斯模糊,行為是把圖片調整得較為模糊,由於原始圖片比較難看出來,這邊放大了煙囪的部分,可以看到右邊的圖片比較模糊。

RandomApply
可以設定單一或多個行為的執行機率,透過 RandomApply 可以讓每個 epoch 拿到的圖片都不一樣,把上面的操作都包起來,每個執行機率設定為 0.5 ,可以看到底下執行出來的結果。

Implementation

因為大部分的 code 都跟這篇差不多,這邊就不細講了,稍微不一樣的地方有兩處,一個是 Model 這次用 ResNet50,另一個是 Dataset 這次使用 CIFAR10 ,由於整個資料量太大,只取了其中的 10% 來使用。

另外把 Resize 以及 RandomCrop 搬到外面進行,習慣上會把每張圖都要做的事情跟隨機要做的事情分開,以便日後修正。


import torch
from torchvision import models, datasets
import torchvision.transforms as T
from torch.utils.data import DataLoader
import torch.utils.data as data
from tqdm.auto import tqdm
from tqdm import tqdm
import torch.nn as nn
import torch.nn.functional as F

input_size = 224
num_classes = 10

transform_set = [
T.RandomApply([T.RandomRotation(90)], p=0.5),
T.RandomApply([T.ColorJitter(brightness=(0, 5), contrast=(0, 5), saturation=(0, 5), hue=(-0.1, 0.1))], p=0.5),
T.RandomApply([T.GaussianBlur(101)], p=0.5),
T.RandomApply([T.RandomHorizontalFlip(1)], p=0.5),
T.RandomApply([T.RandomVerticalFlip(1)], p=0.5),
]

train_tfm = T.Compose([
T.Resize(256),
T.RandomCrop((input_size, input_size)),
T.RandomApply(transform_set, p=1),
T.ToTensor(),
T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

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

device = "cuda" if torch.cuda.is_available() else "cpu"
print('Training on', device)

# get dataset
full_data = datasets.CIFAR10(
root="data",
train=True,
download=True,
transform=train_tfm
)

trash_data_size = int(len(full_data)*0.9)
partial_data_size = int(len(full_data) - trash_data_size)
train_data_size = int(partial_data_size * 0.9)
val_data_size = partial_data_size - train_data_size
trash_data, partial_data = data.random_split(full_data, [trash_data_size, partial_data_size])
train_data, val_data = data.random_split(partial_data, [train_data_size, val_data_size])

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

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 = models.resnet50(weights=models.ResNet50_Weights.DEFAULT)
num_features = model.fc.in_features
model.fc = nn.Linear(num_features, num_classes)

model = model.to(device)

num_epochs = 10
lr = 0.001
optimizer = torch.optim.Adam(model.parameters(), lr)

# start training
for epoch in range(num_epochs):
train_list = []
val_list = []
# 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_list.append(train_acc.item())
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
val_list.append(val_acc.item())

mean_train_acc = sum(train_list) / len(train_list)
mean_val_acc = sum(val_list) / len(val_list)

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

底下左側是執行 10 個 epoch 的結果,右側是我另外用一個簡單的 CNN 來比較,可以看到有 pre-trained model 的結果明顯的比較好,如果能多訓練幾個 epoch 相信能有更多的成長。

Conclusion

以前修課的時候常常都要自己調整 Model 以及參數,常常都是把 Model 放著跑就去忙別的事情,過了幾個小時回來發現訓練的效果太差,再重複的做一樣的事。助教通常會禁止使用 pre-trained model ,希望我們能自己學習如何調整架構,以至於會花非常多的時間做作業。

實驗室的學長曾經跟我說,ML 是一種 life-style ,經過了幾個嚴酷的作業後,終於理解了學長的意思了QQ

包括前面的文章,Image Classification 的介紹應該算是蠻完整的,如果有哪裡寫的不清楚或是少講了什麼內容,請再留言跟我說!!

Reference

--

--