Attention please

[논문 리뷰] VGGNet(2015), 파이토치 구현 본문

논문 리뷰/Image classification

[논문 리뷰] VGGNet(2015), 파이토치 구현

Seongmin.C 2022. 10. 1. 16:17

이번에 구현할 논문은

"VERY DEEP CONVOLUTIONAL NETWORKS FOR LARGE-SCALE IMAGE RECOGNITION" 입니다.

 

이번 논문을 구현하기 위해 사용한 프레임 워크는 Pytorch 입니다.

 

 

 

 

 

The Dataset

논문에서 사용한 데이터셋은 저번 글에서 소개한 ImageNet dataset입니다.

 

하지만 너무 많은 양의 데이터와 분류때문에 학습시간이 오래 걸릴 것을 고려하여

 

저번과 동일하게 CIFAR-10 데이터셋을 사용하도록 하겠습니다.

(imagenet 과 cifar-10 데이터셋에 대한 설명은 저번 글에 남겨놓았습니다.)

 

 

 

 

 

Depth

이 논문에서 중요시하는 것은 모델의 깊이 입니다.

델의 깊이가 깊어지면 깊어질 수록 성능이 좋아진다는 것입니다.

 

즉, 이미지의 대한 정보를 훨씬 더 많이 담을 수 있게 됩니다.

 

 

위의 사진은 논문에서 실험한 모델들의 구조에 대한 설명입니다.

 

 

모델에 입력되는 이미지는 224x224 사이즈이며, 

conv 층과 maxpool층을 반복하다 FC 층에 도달하는 모습을 볼 수 있습니다.

 

저번글에 소개했던 AlexNet에 비해 2배 이상 층이 깊어진 것을 볼 수 있으며,

필터 size를 3x3만 사용했다는 특징이 있습니다.

 

 

 

 

 

3x3 Filter

그렇다면 왜 필터의 사이즈를 3x3만 사용했을까?

 

우선 3x3 필터 2개를 사용하는 것과 5x5 필터 1개를 사용하는 것은 동일합니다.

 

하지만 큰 필터로 한번에 적용하는 것이 아닌

작은 필터로 여러번 적용시키게 되면 conv 층을 통과하는 빈도가 많아지고 

자연스럽게 ReLU층을 통과하는 빈도 역시 늘어납니다.

이로 인해 다양한 데이터에 대해 유연하게 대처가 가능해집니다.

 

즉, 결정함수의 비선형성이 증가하게 됩니다.

 

 

 

또한 작은 사이즈의 필터를 여러개 사용한 것이 큰 필터를 사용한 것에 비해

parameter 수가 줄어들게 되는데 이는 마치 batchnormalization을 적용한 것과 같은 효과를 냅니다.

 

즉 일반화를 적용한 효과를 가지며 overfitting을 줄이는 효과를 가지게 됩니다.

 

 

 

 

 

 

Data preprocessing

학습시킬 데이터의 size는 모델의 input size와 동일해야합니다.

 

VGGNet의 input size는 224x224이였으며,

ImageNet 데이터셋의 크기를 동일하게 해주어야 합니다.

 

논문에서 사용한 방법은 이미지의 가로와 세로중

더 짧은 쪽의 길이를 224로 비율을 지키며 줄여줍니다.

 

그 후에 224x224 size만큼 crop을 해주어 전처리를 진행하였습니다.

 

 

 

 

 

Architecture

VGGNet 의 아키텍처를 시각화한 것입니다.

 

AlexNet 에 비해 layer가 깊어진 것을 확인할 수 있습니다.

 

 

 

 

 

코드구현

이번에 구현할 모델은 VGGNet 모델중 vgg19모델을 구현하겠습니다.

 

또한 사용하는 데이터가 cifar-10이기에 output 쪽만 데이터에 맞게 수정하겠습니다.

 

먼저 사용할 모듈들을 불러오겠습니다

 

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader

import torchvision
import torchvision.datasets
import torchvision.transforms as transforms

from torchsummary import summary as summary_

import numpy as np

 

이어서 gpu를 사용하여 학습을 진행할 것이기 때문에

cuda환경으로 변수를 지정해줍니다.

 

use_cuda = torch.cuda.is_available()
device = torch.device("cuda" if use_cuda else "cpu")

 

데이터 cifar-10 을 불러오도록 하겠습니다.

 

이때 전처리 과정중 저번과 마찬가지로

논문과 최대한 동일하게 하기 위해 데이터의 크기를 227x227로 하려고 하였습니다만

코랩의 환경 특성상 사용할 수 있는 gpu메모리가 한정되어 있어 돌리는 것이 불가능했습니다.

 

위와 같은 이유로 따로 resize없이 실험을 진행하도록 하겠습니다.

 

transform = transforms.Compose([transforms.ToTensor(),
                                transforms.Normalize((0.5,0.5,0.5), (0.5,0.5,0.5))])
                                
trainset = torchvision.datasets.CIFAR10(root='/data',
                                        train=True,
                                        download=True,
                                        transform=transform)

testset = torchvision.datasets.CIFAR10(root='/data',
                                       train=False,
                                       download=True,
                                       transform=transform)
                                       
train_loader = DataLoader(trainset,
                          batch_size=256,
                          shuffle=True,
                          num_workers=2)
test_loader = DataLoader(testset,
                         batch_size=100,
                         shuffle=True,
                         num_workers=2)

 

이제 모델을 만들어보겠습니다.

VGG11, VGG13, VGG16, VGG19 모두 사용할 수 있게 해두었지만

앞서 말했던 것처럼 실험에 사용한 모델을 VGG19 하나뿐입니다.

 

from torch.nn.modules.dropout import Dropout
cfg = {
    'VGG11': [64, 'M', 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'VGG13': [64, 64, 'M', 128, 128, 'M', 256, 256, 'M', 512, 512, 'M', 512, 512, 'M'],
    'VGG16': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 'M', 512, 512, 512, 'M', 512, 512, 512, 'M'],
    'VGG19': [64, 64, 'M', 128, 128, 'M', 256, 256, 256, 256, 'M', 512, 512, 512, 512, 'M', 512, 512, 512, 512, 'M'],
}

class VGG(nn.Module):
  def __init__(self, vgg_name, num_classes=10):
    super(VGG, self).__init__()
    self.features = self._make_layers(cfg[vgg_name])

    self.fc_layer = nn.Sequential(
        # cifar10의 size가 32x32이므로
        nn.Linear(512*1*1, 4096),
        # 만약 imagenet대회 데이터인 224x224이라면
        # nn.Linear(512*7*7, 4096)
        nn.ReLU(inplace=True),
        nn.Dropout(p=0.5),
        nn.Linear(4096, 1000),
        nn.ReLU(inplace=True),
        nn.Dropout(p=0.5),
        nn.Linear(1000, num_classes)
    )

  def forward(self, x):
    out = self.features(x)
    out = out.view(out.size(0), -1)
    out = self.fc_layer(out)

    return out

  def _make_layers(self, cfg):
    layers = []
    in_channels = 3
    for x in cfg:
      if x == 'M':
        layers += [nn.MaxPool2d(kernel_size=2, stride=2)]
      else:
        layers += [nn.Conv2d(in_channels, x, kernel_size=3, padding=1), 
                   nn.BatchNorm2d(x),   
                   # batchnormalization 층을 사용하는 모델들이 요즘 쓰임
                   # overfitting 억제와 학습속도 개선에 용이하다.
                   nn.ReLU(inplace=True)]
        in_channels = x
    

    return nn.Sequential(*layers)

net = VGG('VGG19').to(device)
print(net)

 

이어서 손실함수와 옵티마이저를 설정하도록 하겠습니다.

 

사용한 손실함수는 cross entropy이며, 옵티마이저는 저번 글에서 말한 것처럼

비교를 위해 SGD를 사용하도록 하겠습니다.

 

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4)

 

이제 훈련을 시켜보도록 하겠습니다.

 

file_name = 'vgg19_cifar10.pth'

def train(epoch):
    print('\n[ Train epoch: %d ]' % epoch)
    net.train()
    train_loss = 0
    correct = 0
    total = 0

    for batch_idx, (inputs, labels) in enumerate(train_loader):
        inputs, labels = inputs.to(device), labels.to(device)
        optimizer.zero_grad()

        outputs = net(inputs)
        loss = criterion(outputs, labels)
        loss.backward()

        optimizer.step()
        train_loss += loss.item()
        _, predicted = outputs.max(1)

        total += labels.size(0)
        current_correct = (predicted == labels).sum().item()
        correct += current_correct

        if batch_idx % 100 == 0:
            print('\nCurrent batch:', str(batch_idx))
            print('Current batch average train accuracy:', current_correct / labels.size(0))
            print('Current batch average train loss:', loss.item() / labels.size(0))            
            

# 훈련이 모두 끝난 후 정확도 / 솔실함수 값을 출력  
    print('\nTotal average train accuarcy:', correct / total)
    print('Total average train loss:', train_loss / total)

def test(epoch):
    print('\n[ Test epoch: %d ]' % epoch)
    net.eval()
    loss = 0
    correct = 0
    total = 0

    for batch_idx, (inputs, labels) in enumerate(test_loader):
        inputs, labels = inputs.to(device), labels.to(device)
        total += labels.size(0)

        outputs = net(inputs)
        loss += criterion(outputs, labels).item()

        _, predicted = outputs.max(1)
        correct += (predicted == labels).sum().item()

    print('\nTotal average test accuarcy:', correct / total)
    print('Total average test loss:', loss / total)

    state = {
        'net' : net.state_dict()
    }
    if not os.path.isdir('checkpoint'):
        os.mkdir('checkpoint')
    torch.save(state, './checkpoint/' + file_name)
    print('Model Saved!')
    
    
    
import time
import os

start_time = time.time()

for epoch in range(0, 10):
    train(epoch)
    test(epoch)
    print('\tTime elapsed:', time.time()-start_time)

 

 

학습결과


Test Accuracy : 78.52%

Test Loss : 0.00819

걸린 시간 : 약 5분


저번 AlexNet 모델의 정확도가 47%가 나온 것에 비해

훨씬 개선된 수치인 78%가 나왔습니다.

 

 

Comments