데이터 분석/딥러닝

[VGGNet] 논문 리뷰 & 구현 (Pytorch)

개발자 소신 2020. 12. 16. 10:24
반응형

안녕하세요 ! 소신입니다.

 

 

VGGNet 논문 리뷰와 Pytorch를 활용한 구현입니다.

 


VGGNet은 작은 필터로 네트워크층을 깊게 쌓은 모델입니다.

2015년에 발표된 논문으로 당시 엄청 복잡한 구조인 GoogleNet에 비해 구조가 간단하면서

에러는 0.1%밖에 나지않아 큰 주목을 받은 모델입니다.

 

GoogleNet Inception v1

 


제가 생각하는 VGGNet의 키포인트들입니다.

모델링 자체는 간단하지만, 전처리, 후처리 등 여러가지 방법을 사용해서 성능을 끌어올리기 위해 노력했다는 느낌을 많이 받았습니다.

 

전처리에선

이미지를 다양한 크기로 변형하고 Crop(오려내기), Color Shift, Flip등 다양한 전처리를 진행했습니다.

 

모델링

Convolutional Layer를 3x3 필터에 Padding=1로 원본 크기에는 변화를 주지 않았습니다.

대신에 Max-pooling을 사용해 사이즈를 절반으로 줄여나가면서 특징들을 추출했습니다.

각각의 Conv. Layer뒤에 Activation Function(활성화 함수)으로 ReLU를 사용했고

Fully Connected Layer로 4096, 4096, num_classes = 1000으로 세 개의 층을 쌓았고 

중간중간에 Dropout을 0.5로 주었습니다.

최종 결과는 Softmax를 했구요

 

최적화

Batch Size 256, momentum 0.9를 주었고 (근데 당시 GPU 메모리가 되나? 암튼 신기...)

Learning rate는 0.01로 주었습니다.

validation 개선이 없을 시 lr에 0.1을 곱했고 총 3번 곱했습니다.

 

테스트

우선 하나의 이미지를 [256=Smin, 384=(Smin+Smax)/2, 512=Smax]로 Rescale합니다.

그리고 각각을 가로로만 flipping하고 5x5 regular grid로 Multi Crop합니다.

5x5 Regular Grid가 무엇을 뜻하는지는 아직 잘 이해가 안되는데

(Rescale한 Image를 5x5 그리드에 투영해 하나씩 가져온다는 느낌인가 싶습니다...)

위의 과정을 거치면 총 150장 (3x2x5x5)의 Test Image를 얻을 수 있습니다.

 

Dense Evaluation은 Conv 1x1를 써서 공간정보를 가져오는 방법으로 구현은 하지 않았습니다.

앙상블로는 위의 테스트 이미지를 여러 모델로 예측한 뒤 각각 합치고,

결과를 SoftMax하는 Soft Voting을 사용했습니다.

 


논문 구현은 VGG-A와 VGG-E만 했는데 모델링은 쉽습니다.

Convolutional Layer와 ReLU를 같이 쌓고, MaxPool 반복, 마지막 Fully Connected Layer는 같습니다.

 


논문 결과

결과적으로 VGGNet은 적은 Epoch에서 일찍 수렴하는 경향을 보였고

이미지를 Rescale, Multi-Crop해 여러개로 늘려서 (Augmentation) 예측한 결과가 더 좋다는 것

성능은 당시 1등모델보다 에러가 0.1%밖에 높지 않은데 구조는 엄청나게 간단하다는 것

 


논문 구현

구현 시 실제 논문과 다르게 진행한 부분입니다.

이미지는 STL10을 사용했고 (데이터셋 링크)

Batch Size도 줄이고.. Rescale도 256x256 하나로만했습니다.

Test이미지도 늘리지 않고 Train Transformer를 그대로 사용했고

학습한 모델 2개로 예측한 결과를 합친 뒤 Softmax하는 앙상블방법을 사용했습니다.

(모델로 예측한 결과는 각 Class에 대한 확률을 나타냅니다. 그래서 합친 뒤 Softmax로 합을 1로 만들었습니다.)

 


# Transform 정의, 데이터 셋 준비

#
import torch
print(torch.__version__)

##################### Transform, Data Set 준비
import torchvision
import torchvision.transforms as transforms

transform = transforms.Compose([
    transforms.Resize(256),
    transforms.RandomCrop(224),
    transforms.ToTensor(),
    transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
])

trainset = torchvision.datasets.STL10(root='./data', split='train', download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True)

testset = torchvision.datasets.STL10(root='./data', split='test', download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False)

Transform과 준비한 Data Set입니다.

데이터를 불러오는 코드는 참조 부분에 있는 글의 코드를 가져왔고, Transform만 살짝 수정했습니다.

 

 


# VGG-A 모델링

#
import torch
import torch.nn as nn
class VGG_A(nn.Module):
    def __init__(self, num_classes: int = 1000, init_weights: bool = True):
        super(VGG_A, self).__init__()
        self.convnet = nn.Sequential(
            # Input Channel (RGB: 3)
            nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, padding=1, stride=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2), # 224 -> 112
            
            nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, padding=1, stride=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2), # 112 -> 56
            
            nn.Conv2d(in_channels=128, out_channels=256, kernel_size=3, padding=1, stride=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(in_channels=256, out_channels=256, kernel_size=3, padding=1, stride=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2), # 56 -> 28

            nn.Conv2d(in_channels=256, out_channels=512, kernel_size=3, padding=1, stride=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1, stride=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2), # 28 -> 14

            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1, stride=1),
            nn.ReLU(inplace=True),
            nn.Conv2d(in_channels=512, out_channels=512, kernel_size=3, padding=1, stride=1),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(kernel_size=2, stride=2), # 14 -> 7
        )

        self.fclayer = nn.Sequential(
            nn.Linear(512 * 7 * 7, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),
            nn.Linear(4096, 4096),
            nn.ReLU(inplace=True),
            nn.Dropout(p=0.5),
            nn.Linear(4096, num_classes),
            # nn.Softmax(dim=1), # Loss인 Cross Entropy Loss 에서 softmax를 포함한다.
        )
    
    def forward(self, x:torch.Tensor):
        x = self.convnet(x)
        x = torch.flatten(x, 1)
        x = self.fclayer(x)
        return x

 

모델링 부분입니다.

__init__과, forward부분을 구현해야하고, 

super().__init__()로 nn.Module의 내용물을 가져와야 합니다.

__init__에서 필터맵을 처리하는 convNet과 classifify를 위한 fclayer를 따로 만들어주었고

forward에서 convnet과 fclayer 사이에 flatten으로 펴주었습니다.

 


# Train

#
vgg11 = VGG_A(num_classes = 10)
vgg11 = vgg11.to(device)

classes =  ('airplance', 'bird', 'car', 'cat', 'deer', 'dog', 'horse', 'monkey', 'ship', 'truck')
import torch.optim as optim

criterion = nn.CrossEntropyLoss().cuda()
optimizer = optim.Adam(vgg11.parameters(),lr=0.00001)

start_time = time.time()
for epoch in range(10):  # loop over the dataset multiple times
    running_loss = 0.0
    for i, data in enumerate(trainloader, 0):
        # get the inputs
        inputs, labels = data
        inputs, labels = inputs.to(device), labels.to(device)
        # zero the parameter gradients
        optimizer.zero_grad()

        # print(inputs.shape)  
        outputs= vgg11(inputs)

        # print(outputs.shape)
        # print(labels.shape)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        # print statistics
        running_loss += loss.item()
        if i % 50 == 49:    # print every 50 mini-batches
            print('[%d, %5d] loss: %.3f' %
                  (epoch + 1, i + 1, running_loss / 50))
            running_loss = 0.0

print(time.time()-start_time)
print('Finished Training')

사실 ImageNet도 직접 해볼려고 Albumentations Transform, DataSet 정의부터 DataLoader까지 다했었는데 클래스 개수가 너무 많아서 다른데이터셋 찾아보니까 코드를 가져다 쓴 느낌..

Pytorch DataSet과 DataLoader, Albumentations 사용방법은 따로 작성해야지...

 


# Validation

#
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
    for data in testloader:
        images, labels = data
        images = images.cuda()
        labels = labels.cuda()
        outputs = vgg19(images)
        _, predicted = torch.max(outputs, 1)
        c = (predicted == labels).squeeze()
        for i in range(4):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1


for i in range(10):
    print('Accuracy of %5s : %2d %%' % (
        classes[i], 100 * class_correct[i] / class_total[i]))

 

Testing 코드입니다.

각 class별로 정확도를 출력하게 됩니다.


# 결과

가장 좌측은 참조 링크에 있는 모델을 그대로 구현해서 학습시킨 결과이고

오른쪽 두 개의 모델은 직접 구현해본 모델입니다.

16층보다 11층이 더 그러려니 하는데 19층이 16층보다 학습시간이 빠르더라구요

심지어 정확도는 11층이 16층보다 높았습니다.

 

이유를 곰곰히생각해봤는데..

참조한 모델은 FC Layer를 avg pooling과 한개 Linear 층만 사용했고,

논문의 모델은 Linear 3개와 2개의 Dropout Layer를 사용했더라구요

10에폭상에서는 빠르기와 성능이 더 좋았지만 Epoch이 늘어난다면 어떻게 될지... 

 

맨 오른쪽에 직접 쌓은 19층은 처음 weight를 잘못잡았는지 로스가 줄어드는데 꽤 오래걸리더라구요

seed를 잘못설정해서 그런가..


# 앙상블 

#
class MyEnsemble(nn.Module):
    def __init__(self, modelA, modelB, num_classes):
        super(MyEnsemble, self).__init__()
        self.modelA = modelA
        self.modelB = modelB
        
    def forward(self, inputs):
        x1 = self.modelA(inputs)
        x2 = self.modelB(inputs)
        x = x1 + x2
        x = nn.Softmax(dim=1)(x)
        return x
        
# Create models and load state_dicts    
modelA = VGG_A(num_classes=10)
modelB = VGG_A(num_classes=10)
# Load state dicts
modelA.load_state_dict(torch.load('/content/drive/MyDrive/Study_AI/vgg11.pth'))
modelB.load_state_dict(torch.load('/content/drive/MyDrive/Study_AI/vgg11_B.pth'))

model = MyEnsemble(modelA, modelB, num_classes=10)
model = model.to(device)

class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
    for data in testloader:
        images, labels = data
        images = images.cuda()
        labels = labels.cuda()
        outputs = model(images)
        _, predicted = torch.max(outputs, 1)
        c = (predicted == labels).squeeze()
        for i in range(4):
            label = labels[i]
            class_correct[label] += c[i].item()
            class_total[label] += 1

for i in range(10):
    print('Accuracy of %5s : %2d %%' % (
        classes[i], 100 * class_correct[i] / class_total[i]))

모델 두 개를 앙상블하는 코드입니다.

예측한 결과를 합쳐서 softmax만 해주었고

 

Transforms를

하나는 Resize(224, 224)만 (왼쪽)

다른 하나는 Resize(256,256), RandomCrop(224,224)로 처리 했을 때 (가운데)

각각 평균 정확도가 42.3%, 40.3%였는데

두개를 앙상블한 결과는 43.3%였습니다. (오른쪽)

전체적으로 잘맞추는건 따라가고, 못맞춘것도 전반적으로 개선한 느낌입니다.

 

완전 똑같이 따라하고 싶었는데 쥐꼬리만한 구글드라이브 용량과, 3년된 당시 가성비 그래픽카드 1050ti로는...

 

감사합니당


Ref.

VGGNet Article

Pytorch Tutorial

VGG Net과 CAM 구현

반응형