มาสร้าง VGG-16 โดยใช้ PyTorch กัน
บทความนี้เป็นบทความต่อเนื่อง ในซีรีย์ “สร้างโมเดล โดย PyTorch” ซึ่งบทความก่อนหน้านี้ ก็ได้ทดลองสร้าง LeNet5 และ AlexNet กันไปแล้ว สำหรับในบทความนี้เราจะสร้าง VGG-16 ซึ่งเป็นหนึ่งในโมเดลที่ “ก้าวหน้า” เนื่องจากเป็นโมเดลที่มีความลึกมากขึ้น โดยมีจำนวน Layer สูงถึง 16 Layer และมีแนวคิดหลายอย่างที่น่าสนใจ ที่เป็นมรดกตกทอดมาจนถึงโมเดลในทุกวันนี้
เราจะเริ่มด้วยการสำรวจและทำความเข้าใจกับสถาปัตยกรรมของ VGG16 จากนั้นก็จะเริ่มเขียนโปรแกรมกัน โดยบทความนี้จะยังคงใช้ dataset CIFAR10 เพื่อจะได้เปรียบเทียบความสามารถเมื่อเทียบกับ AlexNet ในบทความที่ผ่านมา
VGG-16
VGG สร้างบนพื้นฐานของ AlexNet ในแง่ของส่วนประกอบ VGG ไม่ได้มีอะไรใหม่ ยังคงใช้ส่วนประกอบตาม AlexNet แต่ VGG ไป focus ในด้านอื่นๆ เพิ่มเติม โดยเฉพาะในด้านของความลึก ซึ่งจะกล่าวถึงต่อไป
VGG พัฒนาโดย Visual Geometry Group ที่ Oxford University
(จึงได้ชื่อว่า VGG) โดยมีกำลังหลักอยู่ 2 คน ได้แก่ Simonyan และ Zisserman พัฒนาขึ้นในปี 2014 แนวคิดที่สำคัญของ VGG มีดังนี้
- มีโครงสร้างที่ง่าย จากโครงสร้างของ CNN ก่อนหน้านี้ จะเห็นว่ามีโครงสร้างที่มีความหลากหลายมาก เช่น kernel size ของ AlexNet มีทั้ง 11x11, 5x5 และ 3x3 ทำให้มี padding, strides หลายค่าไปด้วย ใน VGG ตัดสินใจว่า kernel size จะใช้ค่าเดียวคือ 3x3 โดยมี strides=1 เสมอ และสำหรับ pooling layer จะใช้ขนาด 2x2 ค่าเดียว โดยมี strides=2 เสมอ โดย kernel size ที่กำหนดเป็น 3x3 เนื่องจากจะได้ค้นหา feature ที่ละเอียด (เมื่อเปรียบเทียบจาก 3x3 ทำให้เกิด feature 1 ค่า กับ 11x11 ทำให้เกิด feature 1 ค่าเท่ากัน)
- จากแนวคิดข้างต้น ทำให้สามารถนำ convolutional layer มา stack รวมกันเป็นชั้นๆ ได้ตามรูป สมมติฐานของ VGG คือ การใช้ kernel size ขนาดเล็ก แต่เพิ่มความลึกของเครือข่าย น่าจะทำให้สามารถเรียนรู้ feature ที่ซับซ้อนได้ดีกว่า โดยใช้การประมวลผลที่น้อยกว่า
- จากรูปข้างต้นจะเห็นว่ามีการใช้ kernel size ขนาด 3x3 ทั้งหมด แต่เพิ่มจำนวนแผ่น feature เข้าไป นอกจากนั้นยังมีการทำเป็น block เช่น block #1 มีการทำ convolution 2 ครั้งจึงทำ pooling 1 ครั้ง แบบนี้ไปเรื่อยๆ ดังนั้นโครงสร้างของ VGG จึงทำความเข้าใจได้ง่าย
- ลองมาเปรียบเทียบระหว่าง AlexNet กับ VGG ใน AlexNet ที่มี kernel size 7x7 จำนวน 1 ชั้น = 49xC² แต่ใน VGG จะใช้ kernel size 3x3 จำนวน 2 ชั้นแทน โดย 1 ชั้น = 3x3 = 9xC² (C=จำนวน channel) หากใช้ 3 ชั้น = 27xC² เมื่อเปรียบเทียบระหว่าง 49xC² กับ 27xC² จะเห็นว่าใน VGG จะใช้พารามิเตอร์ลดลงไปถึง 45%
- และด้วยโครงสร้างที่ง่ายจึงทำให้ VGG สามารถปรับเปลี่ยน configuration โดยนอกจาก VGG-16 แล้วยังมีโครงสร้างแบบอื่นๆ ดังตาราง แต่โดยทั่วไปจะใช้ VGG-16 หรือ VGG-19 หากสนใจ paper สามารถอ่านต้นฉบับได้ here
Dataset และ DataLoader
บทความนี้จะยังคงใช้ชุดข้อมูล CIFAR-10 ซึ่งประกอบด้วยข้อมูลภาพสี 60,000 ภาพ (RGB) ที่มีขนาด 32x32 จุด โดยแบ่งออกเป็น 10 ประเภท (class) ประเภทละ 6,000 ภาพ โดยจะแบ่งข้อมูลเป็น 2 ส่วน คือ ข้อมูลสำหรับสอนโมเดล จำนวน 50,000 ภาพ และ ข้อมูลสำหรับทดสอบ จำนวน 10,000 ภาพ
กำหนด hyperparameter ได้แก่
- batch_size เป็นขนาด 16 หมายความว่าจะนำภาพเข้าไปในโมเดลครั้งละ 16 ภาพโดยภาพมีขนาด 32x32x3
- num_class = 10 หมายความว่าภาพที่จะให้ทำนายมี 10 ประเภท หรือ 10 class
- learning_rate = 0.0001 กำหนดอัตราการเรียนรู้เท่ากับ 0.0005
- num_epochs = 50 กำหนดจำนวนรอบของการสอนโมเดล 20 รอบ
โหลดข้อมูลเพื่อนำมา train โดยจะใช้โมดูล dataset ที่อยู่ใน torchvision โดยโมดูลนี้จะช่วยในการโหลด dataset ให้มาอยู่ในเครื่องคอมพิวเตอร์ (./data) โดยในระหว่างโหลดสามารถแปลงรูปภาพ (transformation) ได้ด้วย จากโปรแกรม คือ แปลงให้เป็น Tensor และ Normalize ระดับสีของแต่ละสีให้เหมาะสม
เราจะสร้าง dataset เป็น 3 ชุด โดยชุดหนึ่งสำหรับฝึกสอนโมเดล (training set) อีกชุดหนึ่งสำหรับทดสอบความถูกต้อง (validation set) และชุดสุดท้ายสำหรับทดสอบความถูกต้อง โดยแบ่งในอัตรา 80:10:10 โดยในการสร้าง training set เขียนให้สามารถเลือกได้ว่าจะใช้ data augmentation หรือไม่ โดยหากใช้ก็จะมีการทำ RandomCrop และ RandomHorizontalFlip จากนั้นส่ง dataset ต่อไปยัง dataloader ซึ่งจะทำหน้าที่จัดข้อมูลเป็น batch เพื่อส่งเข้าไปในโมเดลครั้งละ 1 batch
นอกจากนั้นยังมีการขยายขนาดภาพจาก 32x32 เป็น 227x227
import numpy as np
import torch
import torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import Dataset, DataLoader, SubsetRandomSampler
import matplotlib.pyplot as plt
# Define relevant variables for the ML task
batch_size = 16
num_classes = 10
learning_rate = 0.0005
num_epochs = 20
# Device configuration
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
torch.manual_seed(42)
torch.cuda.manual_seed_all(42)
class CIFAR10Dataset(Dataset):
def __init__(self, data_dir, train=True, augment=False, valid_size=0.1, random_seed=42):
self.data_dir = data_dir
self.train = train
self.augment = augment
self.valid_size = valid_size
self.random_seed = random_seed
self.normalize = transforms.Normalize(
mean=[0.4914, 0.4822, 0.4465],
std=[0.2023, 0.1994, 0.2010],
)
if self.train:
if self.augment:
self.transform = transforms.Compose([
transforms.RandomCrop(32, padding=4),
transforms.RandomHorizontalFlip(),
transforms.Resize((227, 227)),
transforms.ToTensor(),
self.normalize,
])
else:
self.transform = transforms.Compose([
transforms.Resize((227, 227)),
transforms.ToTensor(),
self.normalize,
])
else:
self.transform = transforms.Compose([
transforms.Resize((227, 227)),
transforms.ToTensor(),
self.normalize,
])
self.dataset = datasets.CIFAR10(
root=self.data_dir, train=self.train,
download=True, transform=self.transform,
)
if self.train:
num_train = len(self.dataset)
indices = list(range(num_train))
split = int(np.floor(self.valid_size * num_train))
np.random.seed(self.random_seed)
np.random.shuffle(indices)
self.train_idx, self.valid_idx = indices[split:], indices[:split]
def __len__(self):
return len(self.dataset)
def __getitem__(self, idx):
return self.dataset[idx]
def get_train_sampler(self):
return SubsetRandomSampler(self.train_idx)
def get_valid_sampler(self):
return SubsetRandomSampler(self.valid_idx)
def get_data_loaders(data_dir, batch_size, augment=False, valid_size=0.1, random_seed=42):
train_dataset = CIFAR10Dataset(data_dir, train=True, augment=augment, valid_size=valid_size, random_seed=random_seed)
test_dataset = CIFAR10Dataset(data_dir, train=False)
train_loader = DataLoader(
train_dataset, batch_size=batch_size, sampler=train_dataset.get_train_sampler()
)
valid_loader = DataLoader(
train_dataset, batch_size=batch_size, sampler=train_dataset.get_valid_sampler()
)
test_loader = DataLoader(
test_dataset, batch_size=batch_size, shuffle=True
)
return train_loader, valid_loader, test_loader
# Usage:
train_loader, valid_loader, test_loader = get_data_loaders(
data_dir='./data',
batch_size=batch_size,
augment=True,
random_seed=42
)
VGG-16
คราวนี้มาดูโปรแกรมส่วนของโมเดล โปรแกรมนี้เขียนให้อ่านได้ง่ายโดยแบ่งเป็น Layer ที่ชัดเจน
class VGG16(nn.Module):
def __init__(self, num_classes=10):
super(VGG16, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU())
self.layer2 = nn.Sequential(
nn.Conv2d(64, 64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2))
self.layer3 = nn.Sequential(
nn.Conv2d(64, 128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(128),
nn.ReLU())
self.layer4 = nn.Sequential(
nn.Conv2d(128, 128, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2))
self.layer5 = nn.Sequential(
nn.Conv2d(128, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU())
self.layer6 = nn.Sequential(
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU())
self.layer7 = nn.Sequential(
nn.Conv2d(256, 256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2))
self.layer8 = nn.Sequential(
nn.Conv2d(256, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU())
self.layer9 = nn.Sequential(
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU())
self.layer10 = nn.Sequential(
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2))
self.layer11 = nn.Sequential(
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU())
self.layer12 = nn.Sequential(
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU())
self.layer13 = nn.Sequential(
nn.Conv2d(512, 512, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(kernel_size = 2, stride = 2))
self.fc = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(7*7*512, 4096),
nn.ReLU())
self.fc1 = nn.Sequential(
nn.Dropout(0.5),
nn.Linear(4096, 4096),
nn.ReLU())
self.fc2= nn.Sequential(
nn.Linear(4096, num_classes))
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = self.layer3(out)
out = self.layer4(out)
out = self.layer5(out)
out = self.layer6(out)
out = self.layer7(out)
out = self.layer8(out)
out = self.layer9(out)
out = self.layer10(out)
out = self.layer11(out)
out = self.layer12(out)
out = self.layer13(out)
out = out.reshape(out.size(0), -1)
out = self.fc(out)
out = self.fc1(out)
out = self.fc2(out)
return out
- ชั้นที่ 1 ใช้ kernel ขนาด 3x3 จำนวน 64 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU
- ชั้นที่ 2 ใช้ kernel ขนาด 3x3 จำนวน 64 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU อีกครั้งจากนั้น max pooling 2x2
- ชั้นที่ 3 ใช้ kernel ขนาด 3x3 จำนวน 128 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU
- ชั้นที่ 4 ใช้ kernel ขนาด 3x3 จำนวน 128 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU อีกครั้งจากนั้น max pooling 2x2
- ชั้นที่ 5 ใช้ kernel ขนาด 3x3 จำนวน 256 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU
- ชั้นที่ 6 ใช้ kernel ขนาด 3x3 จำนวน 256 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU
- ชั้นที่ 7 ใช้ kernel ขนาด 3x3 จำนวน 256 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU อีกครั้งจากนั้น max pooling 2x2
- ชั้นที่ 8 ใช้ kernel ขนาด 3x3 จำนวน 512 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU
- ชั้นที่ 9 ใช้ kernel ขนาด 3x3 จำนวน 512 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU
- ชั้นที่ 10 ใช้ kernel ขนาด 3x3 จำนวน 512 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU อีกครั้งจากนั้น max pooling 2x2
- ชั้นที่ 11 ใช้ kernel ขนาด 3x3 จำนวน 512 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU
- ชั้นที่ 12 ใช้ kernel ขนาด 3x3 จำนวน 512 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU
- ชั้นที่ 13 ใช้ kernel ขนาด 3x3 จำนวน 512 แผ่น ด้วย stride=1, padding=1 จากนั้นทำ batch normalization และ ReLU อีกครั้งจากนั้น max pooling 2x2
- ชั้นที่ 14 จะเหลือขนาดภาพเป็น 7x7 จำนวน 512 แผ่น เป็น input เข้าสู่ Fully Connected Layer 4096 โหนด
- ชั้นที่ 15 Fully Connected Layer 4096 โหนด
- ชั้นที่ 16 Fully Connected Layer จำนวนเท่ากับ output class
- มีจำนวนพารามิเตอร์ทั้งหมด 138 ล้านตัว มากกว่า AlexNet ที่มี 60 ล้านตัว ประมาณ 2.3 เท่า
การสอน model (Training)
ยังคงใช้โครงสร้างเดียวกับโปรแกรมที่ผ่านมา โดยกำหนด epoch เป็น 20 epoch โดยฟังก์ชันที่ทำหน้าที่ train model มีดังนี้
def train_model(model, train_loader, valid_loader, criterion, optimizer, epochs=20, device='cuda'):
training_logs = {
"train_loss": [], "train_acc": [], "validate_loss": [], "validate_acc": []
}
for epoch in range(epochs):
# Training phase
model.train()
running_loss = 0.0
correct = 0
total = 0
for images, labels in train_loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
running_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
train_loss = running_loss / len(train_loader)
train_accuracy = 100 * correct / total
training_logs["train_loss"].append(train_loss)
training_logs["train_acc"].append(train_accuracy)
# Validation phase
model.eval()
running_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in valid_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
running_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
valid_loss = running_loss / len(valid_loader)
valid_accuracy = 100 * correct / total
training_logs["validate_loss"].append(valid_loss)
training_logs["validate_acc"].append(valid_accuracy)
print(f'Epoch [{epoch+1}/{epochs}] :: ',end='')
print(f'Train Loss: {train_loss:.4f}, Train Accuracy: {train_accuracy:.2f}% ',end='')
print(f'Valid Loss: {valid_loss:.4f}, Valid Accuracy: {valid_accuracy:.2f}%')
print('-' * 80)
return training_logs
ขออธิบาย code ในส่วนของฟังก์ชัน train_model
- ฟังก์ชันจะรับพารามิเตอร์ model , train_loader, test_loader, criterion, optimizer และ epochs
- model คือ ตัว instance ของโมเดลที่สร้างขึ้นจาก class
- train_loader และ test_loader คือ ตัวโหลดข้อมูลเข้ามาทีละ batch เพื่อเข้ามา train ในแต่ละรอบ
- criterion คือ loss function ที่ใช้คำนวณว่าการทำงานมีผลลัพธ์ใกล้กับสิ่งที่ต้องการมากน้อยเพียงใด
- optimizer คือ ฟังก์ชันที่ทำการคำนวณค่าพารามิเตอร์ที่จะต้องปรับในโมเดล
- epochs คือ จำนวนรอบในการทำงาน
- ในบรรทัดแรกจะมีการเตรียมพื้นที่สำหรับเก็บผลการทำงานในแต่ละรอบ โดยเก็บใน training_logs
- จากนั้นจะเข้าสู่ loop แรก ซึ่งจะทำงานตามจำนวน epochs และภายใน loop นอกนี้จะแบ่งการทำงานเป็น 2 ส่วน คือ ส่วนของ training loop และ evaluation loop
- ใน loop ในที่ 1 เป็น training loop จะรับข้อมูลภาพและ label มาจาก train loader จากนั้นจะนำเข้าสู่ device ซึ่งอาจเป็น GPU หรือ CPU
- จากนั้นจะเป็นการทำ forward pass โดยให้ model ทำการทำนายตามข้อมูลใน model โดยจะได้ข้อมูลกลับมาใน output ซึ่งจะนำไปคำนวณค่า loss ในบรรทัด
loss=criterion(output, labels)
- จากนั้นจึงทำ backward pass โดยเริ่มจากเคลียรข้อมูล gradient จากการทำงานในรอบก่อนหน้าออก
optimizer.zero_grad()
- จากนั้นนำค่า loss ไปคำนวณค่า gradient ใหม่สำหรับปรับพารามิเตอร์ โดยใช้ฟังก์ชัน
loss.backward()
- และสุดท้ายนำไปปรับ weights ในฟังก์ชัน
optimizer.step()
- ใน loop ในที่ 2 จะเป็น loop สำหรับนำข้อมูลที่ใช้ประเมิน evaluation data มาทดสอบกับโมเดล จะได้เห็นภาพว่าในแต่ละ epoch โมเดลมีการปรับตัวดีขึ้นมากน้อยเพียงใด โดยการทำงานจะคล้ายกับ training loop ทุกประการ จะต่างกันตรงที่จะไม่มีการปรับ weights
- ส่วนสุดท้ายจะเป็นการเก็บข้อมูลของแต่ละ epoch ลงใน training_logs
การสอน model (Training)
หลังจากที่สร้างฟังก์ชันสำหรับ train แล้ว ก็มาทำการสอนโมเดลกัน โดยกำหนด loss function ซึ่งในที่นี้จะใช้ CrossEntropyLoss เนื่องจากเป็นการทำงานแบบ multiple classification และเลือก optimization algorithm ซึ่งจะเลือกฟังก์ชัน SDG (Stochastic Gradient Descent) ในการทำงาน เมื่อพร้อมแล้วก็เรียกฟังก์ชัน train model มาทำงาน
model = VGG16(num_classes).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, weight_decay=0.005, momentum=0.9)
training_logs = train_model(model, train_loader, valid_loader, criterion, optimizer, epochs=num_epochs)
ซึ่งจะแสดงผลลัพธ์ดังนี้
Epoch [1/20] :: Train Loss: 1.5749, Train Accuracy: 41.62% Valid Loss: 1.2315, Valid Accuracy: 54.94%
--------------------------------------------------------------------------------
Epoch [2/20] :: Train Loss: 1.0892, Train Accuracy: 60.98% Valid Loss: 0.9462, Valid Accuracy: 66.22%
--------------------------------------------------------------------------------
Epoch [3/20] :: Train Loss: 0.8666, Train Accuracy: 69.34% Valid Loss: 0.7737, Valid Accuracy: 72.78%
--------------------------------------------------------------------------------
Epoch [4/20] :: Train Loss: 0.7460, Train Accuracy: 73.85% Valid Loss: 0.6860, Valid Accuracy: 75.58%
--------------------------------------------------------------------------------
Epoch [5/20] :: Train Loss: 0.6665, Train Accuracy: 76.87% Valid Loss: 0.6257, Valid Accuracy: 78.56%
--------------------------------------------------------------------------------
Epoch [6/20] :: Train Loss: 0.6056, Train Accuracy: 79.18% Valid Loss: 0.5563, Valid Accuracy: 80.52%
--------------------------------------------------------------------------------
Epoch [7/20] :: Train Loss: 0.5508, Train Accuracy: 80.98% Valid Loss: 0.5237, Valid Accuracy: 81.70%
--------------------------------------------------------------------------------
Epoch [8/20] :: Train Loss: 0.5218, Train Accuracy: 82.26% Valid Loss: 0.4936, Valid Accuracy: 82.78%
--------------------------------------------------------------------------------
Epoch [9/20] :: Train Loss: 0.4885, Train Accuracy: 83.12% Valid Loss: 0.4757, Valid Accuracy: 83.28%
--------------------------------------------------------------------------------
Epoch [10/20] :: Train Loss: 0.4603, Train Accuracy: 84.08% Valid Loss: 0.4473, Valid Accuracy: 85.04%
--------------------------------------------------------------------------------
Epoch [11/20] :: Train Loss: 0.4338, Train Accuracy: 85.15% Valid Loss: 0.4865, Valid Accuracy: 83.28%
--------------------------------------------------------------------------------
Epoch [12/20] :: Train Loss: 0.4114, Train Accuracy: 85.99% Valid Loss: 0.4590, Valid Accuracy: 84.44%
--------------------------------------------------------------------------------
Epoch [13/20] :: Train Loss: 0.3913, Train Accuracy: 86.68% Valid Loss: 0.4482, Valid Accuracy: 85.00%
--------------------------------------------------------------------------------
Epoch [14/20] :: Train Loss: 0.3766, Train Accuracy: 87.24% Valid Loss: 0.4147, Valid Accuracy: 85.60%
--------------------------------------------------------------------------------
Epoch [15/20] :: Train Loss: 0.3676, Train Accuracy: 87.45% Valid Loss: 0.3897, Valid Accuracy: 86.64%
--------------------------------------------------------------------------------
Epoch [16/20] :: Train Loss: 0.3476, Train Accuracy: 88.00% Valid Loss: 0.4448, Valid Accuracy: 84.70%
--------------------------------------------------------------------------------
Epoch [17/20] :: Train Loss: 0.3346, Train Accuracy: 88.60% Valid Loss: 0.4216, Valid Accuracy: 85.46%
--------------------------------------------------------------------------------
Epoch [18/20] :: Train Loss: 0.3208, Train Accuracy: 89.12% Valid Loss: 0.4382, Valid Accuracy: 84.46%
--------------------------------------------------------------------------------
Epoch [19/20] :: Train Loss: 0.3124, Train Accuracy: 89.24% Valid Loss: 0.3795, Valid Accuracy: 87.34%
--------------------------------------------------------------------------------
Epoch [20/20] :: Train Loss: 0.3008, Train Accuracy: 89.62% Valid Loss: 0.3957, Valid Accuracy: 86.50%
--------------------------------------------------------------------------------
จะเห็นได้ว่าค่า loss ลดลงในทุก epoch ซึ่งแสดงให้เห็นว่า model สามารถเรียนรู้ และทำงานได้ดีขึ้นเรื่อยๆ ตามรอบการทำงาน และผลลัพธ์สุดท้ายออกมาค่อนข้างดี โดยมีอัตราความถูกต้องสูงมากกว่า 85%
การทดสอบ model (Testing)
จะแบ่งออกเป็น 2 ส่วน คือ แสดงโดยกราฟ ซึ่งใช้โปรแกรมต่อไปนี้
def plot_graph(training_logs):
epochs = len(training_logs["train_loss"])
epochs_range = range(1, epochs + 1)
plt.figure(figsize=(12, 5))
# Plot loss
plt.subplot(1, 2, 1)
plt.plot(epochs_range, training_logs["train_loss"], label='Train Loss')
plt.plot(epochs_range, training_logs["validate_loss"], label='Valid Loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.title('Loss vs Epochs')
# Plot accuracy
plt.subplot(1, 2, 2)
plt.plot(epochs_range, training_logs["train_acc"], label='Train Accuracy')
plt.plot(epochs_range, training_logs["validate_acc"], label='Valid Accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy (%)')
plt.legend()
plt.title('Accuracy vs Epochs')
plt.tight_layout()
plt.show()
plot_graph(training_logs)
ซึ่งได้ผลดังนี้
จะเห็นได้ว่าผลการเรียนรู้ค่อนข้างดี และ ไม่มี overfitting
จากนั้นเขียนโปรแกรมเพื่อนำ test set ซึ่งโมเดลยังไม่เคยเห็นภาพมาก่อนมาทดสอบ โดยใช้โปรแกรมต่อไปนี้
def test_model(model, test_loader, criterion, device='cuda'):
model.eval()
running_loss = 0.0
correct = 0
total = 0
with torch.no_grad():
for images, labels in test_loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
running_loss += loss.item()
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
test_loss = running_loss / len(test_loader)
test_accuracy = 100 * correct / total
print(f'Test Loss: {test_loss:.4f}, Test Accuracy: {test_accuracy:.2f}%')
return test_loss, test_accuracy
test_loss, test_accuracy = test_model(model, test_loader, criterion)
ปรากฏผลว่า Test Loss: 0.3228, Test Accuracy: 88.98% ซึ่งถือว่าดีขึ้นกว่า AlexNet ที่มีผลการทำงาน 85.75%
สรุปผล
เราลองมาสรุปผลการทำงานกัน
- เราได้เริ่มจากอธิบายสถาปัตยกรรมของโมเดล VGG ซึ่งจุดเด่นของ VGG คือ โมเดลมีความลึกมากขึ้นกว่าเดิม และมีการใช้ kernel ขนาดเดียว เรียกได้ว่าทำให้การใช้ kernel ขนาดใหญ่หายไปในยุคหลังเลยทีเดียว
- จากนั้นได้อธิบายการแบ่งข้อมูล และ pre-processed CIFAR10 dataset โดยใช้
torchvision
- และใช้
PyTorch
ในการสร้าง VGG-16 - สุดท้ายเราได้ train และ test การทำงาน บน CIFAR10 dataset และโมเดลก็ทำงานได้ดีพอสมควร
ต่อไปเราจะลองโมเดลอื่นๆ กับชุดข้อมูลนี้ว่าจะให้ผลเป็นอย่างไรต่อไป