ML \ DL/PyTorch Zero To All

PyTorch Lecture 11: Advanced CNN

lfgwy 2022. 8. 5. 00:31

방학동안 학회에서 김성훈 교수님의 PyTorch Zero To All 강의로 공부를 하게 된 김에 스스로 정리해보려고 합니다.

좋은 강의 공유해주신 김성훈 교수님께 감사드립니다.

 

강의링크: 

https://www.youtube.com/playlist?list=PLlMkM4tgfjnJ3I-dbhO9JTw7gNty6o_2m 

 

PyTorchZeroToAll (in English)

Basic ML/DL lectures using PyTorch in English.

www.youtube.com

 

Inception Modules

 

CNN을 사용할 때, 주어진 이미지에 대해 다양한 size의 filter를 사용하여 특정 output을 산출할 수 있습니다.

사용가능한 다양한 size의 filter 중 어떤 것을 사용해야할지에 대한 궁금증이 따르는 것은 당연합니다.

이 궁금증에서 Inception Modules에 대한 idea가 출발합니다. 여러 size의 kernel들의 조합을 사용해보는 것입니다.

아래 이미지에서는 5 x 5, 3 x 3, 1 x 1의 3 filter를 사용하고 있습니다. 그 이후 각 filter를 통해 도출된 결과를 concatenate 해줍니다.

PyTorch Zero to All 김성훈 교수님 강의자료

그림을 자세히 살펴보면, 5 x 5, 3 x 3 filter에 적용하기에 앞서, input image에 대해 1 x 1 convolution이 수행됐음을 확인할 수 있습니다.

1 x 1 convolution은 다양한 특징과 이점이 있어 자주 사용됩니다.

 

1 x 1 convolution

 

1 x 1 convolution이 사용되는 가장 대표적인 이유는 channel의 수를 축소하기 위해서입니다. 각 1 x 1 filter는 픽셀당 하나의 값만을 발생시킵니다. 이와 같은 연산을 하기 때문에 output의 size는 변하지 않고, channel의 수만 사용하는 filter의 수와 동일하게 변하게 됩니다.

 

아래 그림은  64 x 56 x 56 의 image에 1개의 1 x 1 filter를 사용하여 1 x 56 x 56로 channe의 수를 줄인 결과입니다.

 

PyTorch Zero to All 김성훈 교수님 강의자료

 

아래 그림은 32개의 1 x 1 filter를 사용하여 채널의 수를 64개에서 32개로 줄인 예시를 보여주고 있습니다.

 

PyTorch Zero to All 김성훈 교수님 강의자료

 

또, 1 x 1 convolution을 사용함으로 요구되는 연산량을 대폭 감소시킬 수 있습니다.

아래 그림에서 왼쪽 convoltution layer는 5 x 5 kernel을 사용하여 32 x 28 x 28의 output을 얻고 있고,

오른쪽 convolution layer는 1 x 1 kernel, 5 x 5 kernel 2개의 kernel을 사용하여 32 x 28 x 28의 output을 얻어내고 있습니다.

 

동일한 output을 산출하면서 2개의 filter를 사용한다는 것이 비효율적으로 보이기도 하고, 어떻게 2개의 filter를 사용하는 쪽의 연산량이 더 적은지 의문이 듭니다.

1 x 1 convolution을 사용하지 않는 왼쪽의 경우 요구되는 총 연산량은 $5^2 \times 28^2 \times 192 \times 32 = 120,422,400$입니다. 하지만 1 x 1 convolution을 사용하는 오른쪽의 경우 거의 $\frac{1}{10}$꼴인 $1^2 \times 28^2 \times 192 \times 16 + 5^2 \times 28^2 \times 16 \times 32 = 12,443,648$의 연산량만이 요구됩니다.

($size of filter \times size of input \times channels_in \times channels_out$)

 

PyTorch Zero to All 김성훈 교수님 강의자료

 

이것이 저희가 Inception Module에서 5 x 5 혹은 3 x 3 filter를 적용하기에 앞서 1 x 1 convolution을 수행하는 이유입니다.

Inception Module을 구현하는 방법은 다음과 같습니다.

 

PyTorch Zero to All 김성훈 교수님 강의자료

 

PyTorch Zero to All 김성훈 교수님 강의자료

 

Deep Residual Learning

Layer을 많이 쌓아 layer가 깊어지면 깊어질수록 모델의 성능이 좋아지는 것으로 보입니다. 하지만 실제로는 그렇지 않습니다.

CIFAR-10에 56개 layer와 20개의 layer를 적용한 결과, 56개의 layer를 적용한 쪽이 train은 물론 test error까지 크게 나타났습니다.

이는 다양한 데이터에서 관측되는 꽤나 일반적인 현상입니다.

 

PyTorch Zero to All 김성훈 교수님 강의자료

 

이런 현상은 다음과 같은 이유로 발생합니다. Layer가 깊어지면서, vanishing gradients problem이 발생합니다. 

Vanishing gradients problem이란, 활성함수의 gradient가 계속 곱해지다 보면 가중치에 따른 결과값의 기울기가 0이 되어 버려서, Gradient Descent(경사하강법)을 이용할 수 없게 되는 문제입니다. 그리고 이렇게 어느정도 이상 깊어진 레이어가 vanishing gradient problem등의 이유로 성능이 더 떨어지는 현상Degradation Problem이라 합니다.

 

이런 문제를 해결할 수 있는 방법 중 하나가 Deep Residual Learning입니다. 

기존의 Plain Net과는 다르게, Deep Residual Learning에서는 추가적으로 Residual Connection이라는 것을 갖습니다.

두 연산은 기본적으로 동일하나 Residual Net에서는 마지막에 input x를 더해줍니다. 이를 Residual Connection이라 합니다. 

 

각 layer의 output H(x)를 살펴봅시다.

Plain Net에서 output H(x)는 x를 통해 새롭게 학습된 정보입니다. 기존에 학습된 정보는 저장되지 않고 새롭게 학습된 정보만이 전달됩니다. 

반면, Residual Net에서 output H(x) = F(x) + x는 기존에 학습된 정보 x와 추가로 학습된 정보인 F(x)를 포함하고 있습니다. 이러한 특성은 많은 장점을 갖는데, 이전에 학습된 모델(레이어들)의 출력과 추가된 레이어의 출력의 차이값인 나머지(residual = F(X))만 학습하면 되기에 연산이 간단해지고, 아무리 미분을 해도 항상 1은 남기 때문에 vanishing gradients problem에서도 자유롭습니다.

 

PyTorch Zero to All 김성훈 교수님 강의자료

Residual Net 사용 시 주의해야할 점이 있는데, residual net안에 다양한 크기의 filter를 갖는 layer를 사용해도 되지만, input x와 새로운 연산의 결과 F(x)를 서로 더해주어야하기 때문에 x와 F(x)의 shape이 서로 같아야한다는 것입니다.

 

또, 깊어지는 layer의 연산량을 줄이기 위한 방법으로 bottleneck이라는 것이 있는데, 이는 layer의 앞뒤로 1 x 1 convolution을 이용하여 연산량을 줄이는 것을 의미합니다. 이런 좁아졌다 넓어지는 모양 때문에 bottleneck이라고 불린다고 합니다. 

아래 그림의 왼쪽이 보통의 residual net을 시각화한 모습이고, 오른쪽이 bottleneck을 적용한 결과입니다.

 

PyTorch Zero to All 김성훈 교수님 강의자료

 

 

# https://github.com/pytorch/examples/blob/master/mnist/main.py
from __future__ import print_function
import argparse
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.autograd import Variable

# Training settings
batch_size = 64

# MNIST Dataset
train_dataset = datasets.MNIST(root='./data/',
                               train=True,
                               transform=transforms.ToTensor(),
                               download=True)

test_dataset = datasets.MNIST(root='./data/',
                              train=False,
                              transform=transforms.ToTensor())

# Data Loader (Input Pipeline)
train_loader = torch.utils.data.DataLoader(dataset=train_dataset,
                                           batch_size=batch_size,
                                           shuffle=True)

test_loader = torch.utils.data.DataLoader(dataset=test_dataset,
                                          batch_size=batch_size,
                                          shuffle=False)


class InceptionA(nn.Module):

    def __init__(self, in_channels):
        super(InceptionA, self).__init__()
        self.branch1x1 = nn.Conv2d(in_channels, 16, kernel_size=1)
        # Inception Module의 두번째(앞으로 서술할 순서는 모두 왼쪽 기준)에 위치한 1x1 conv

        self.branch5x5_1 = nn.Conv2d(in_channels, 16, kernel_size=1)
        self.branch5x5_2 = nn.Conv2d(16, 24, kernel_size=5, padding=2)
        # 세번째에 위치한 1x1 conv -> 5x5 conv

        self.branch3x3dbl_1 = nn.Conv2d(in_channels, 16, kernel_size=1)
        self.branch3x3dbl_2 = nn.Conv2d(16, 24, kernel_size=3, padding=1)
        self.branch3x3dbl_3 = nn.Conv2d(24, 24, kernel_size=3, padding=1)
        # 네번째에 위치한 1x1 conv -> 3x3 conv -> 5x5 conv

        self.branch_pool = nn.Conv2d(in_channels, 24, kernel_size=1)
        # 첫번째에 위치한 Avg Pooling 다음의 1x1 conv

    def forward(self, x):
        branch1x1 = self.branch1x1(x)
        # 두번째 path

        branch5x5 = self.branch5x5_1(x)
        branch5x5 = self.branch5x5_2(branch5x5)
        # 세번째 path

        branch3x3dbl = self.branch3x3dbl_1(x)
        branch3x3dbl = self.branch3x3dbl_2(branch3x3dbl)
        branch3x3dbl = self.branch3x3dbl_3(branch3x3dbl)
        # 네번째 Path

        branch_pool = F.avg_pool2d(x, kernel_size=3, stride=1, padding=1)
        branch_pool = self.branch_pool(branch_pool)
        # 첫번째 path

        outputs = [branch1x1, branch5x5, branch3x3dbl, branch_pool]
        # concatenation
        return torch.cat(outputs, 1)


class Net(nn.Module):

    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(88, 20, kernel_size=5)

        self.incept1 = InceptionA(in_channels=10)
        self.incept2 = InceptionA(in_channels=20)

        self.mp = nn.MaxPool2d(2)
        self.fc = nn.Linear(1408, 10)

    def forward(self, x):
        in_size = x.size(0)
        x = F.relu(self.mp(self.conv1(x)))
        x = self.incept1(x)
        x = F.relu(self.mp(self.conv2(x)))
        x = self.incept2(x)
        x = x.view(in_size, -1)  # flatten the tensor
        x = self.fc(x)
        return F.log_softmax(x)


model = Net()

optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.5)


def train(epoch):
    model.train()
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = Variable(data), Variable(target)
        optimizer.zero_grad()
        output = model(data)
        loss = F.nll_loss(output, target)
        loss.backward()
        optimizer.step()
        if batch_idx % 10 == 0:
            print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format(
                epoch, batch_idx * len(data), len(train_loader.dataset),
                100. * batch_idx / len(train_loader), loss.data[0]))


def test():
    model.eval()
    test_loss = 0
    correct = 0
    for data, target in test_loader:
        data, target = Variable(data, volatile=True), Variable(target)
        output = model(data)
        # sum up batch loss
        test_loss += F.nll_loss(output, target, size_average=False).data[0]
        # get the index of the max log-probability
        pred = output.data.max(1, keepdim=True)[1]
        correct += pred.eq(target.data.view_as(pred)).cpu().sum()

    test_loss /= len(test_loader.dataset)
    print('\nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)\n'.format(
        test_loss, correct, len(test_loader.dataset),
        100. * correct / len(test_loader.dataset)))


for epoch in range(1, 10):
    train(epoch)
    test()

'ML \ DL > PyTorch Zero To All' 카테고리의 다른 글

RNN II (Classification)  (0) 2022.08.14
PyTorch Lecture 12: RNN  (0) 2022.08.09
PyTorch Lecture 10: Basic CNN  (0) 2022.07.26
PyTorch Lecture 09: Softmax Classifier  (0) 2022.07.14
PyTorch Lecture 06: Logistic Regression  (0) 2022.07.13