안녕하세요.

 

 

오늘은 Learning Deep Features for Discriminative Localization이라는 논문에 대해서 정리해보려고 합니다.

 

 

해당 논문에서 제안하는 접근법을 통해 만들어지는 Class Activation Map(CAM)으로도 유명한 논문입니다.

 

 

논문 주소: arxiv.org/abs/1512.04150

 

Learning Deep Features for Discriminative Localization

In this work, we revisit the global average pooling layer proposed in [13], and shed light on how it explicitly enables the convolutional neural network to have remarkable localization ability despite being trained on image-level labels. While this techniq

arxiv.org

 

그럼 시작해보겠습니다.

 

 

Abstract

 

 

본 논문에서 저자들은 global average pooling layer에 대해서 다시 논의하고, 어떻게 이것에 의해서 convolutional neural network (CNN)가 image-level label에 학습되었음에도 불구하고 놀라운 localization ability를 가지도록 만들어주는지에 대해서 논의합니다.

 

Global average pooling은 학습을 규제하기 위한 수단으로써 이전에 제안되었지만, 본 논문의 저자들은 이것이 이미지에 대해서 CNN의 암시된 attention을 드러내는 포괄적인 localizable deep representation을 만들어낸다고 생각하였습니다.

 

Global average pooling의 단순함에도 불구하고, 본 논문의 저자들은 어떠한 bounding box annotation을 이용한 학습을 하지 않고도 ILSVRC 2014 object localization에 대해서 37.1% top-5 error를 달성할 수 있었습니다.

 

저자들은 본 논문에서 network가 classification task를 해결하기 위해서 학습되었음에도 불구하고, discriminative image region의 위치를 알아낼 수 있다는 것을 다양한 실험을 통해서 검증하였습니다.

 

 

 

1. Introduction

 

 

Zhou et al에 의한 최근 연구는 object의 위치에 대한 어떠한 supervision이 제공되지 않았음에도 불구하고 convolutional neural network (CNNs)의 여러 가지 layers의 convolutional units이 실제로 object detector로써 행동한다는 것을 보여주었습니다.

 

Convolutional layers가 object의 위치를 알아낼 수 있는 놀라운 능력을 가지고 있음에도 불구하고, 이러한 능력은 classification을 위해 사용되는 fully-connected layer가 존재할 때 잃게 됩니다.

 

최근에, Network in Network (NIN)와 GoogLeNet과 같은 여러 인기 있는 fully-convolutional neural networks는 좋은 성능을 유지하면서도 parameter의 수를 최소화하기 위해 fully-connected layers의 사용을 피하기 위해 제안되었습니다. (convolution layer에 비해서 fully-connected layer의 weight 수가 훨씬 많기 때문이죠.)

 

 

이를 달성하고자, Zhou et al는 structural regularizer의 역할을 수행하는 global average pooling을 사용하였으며, 이는 학습이 진행되는 도중에 overfitting을 방지합니다.

 

본 논문의 저자들은 실험을 통해 global average pooling의 이점이 단순히 regularizer의 역할을 수행하는 것 이상으로 확장될 수 있다는 사실을 발견하였습니다. 

 

즉, network의 구조를 약간 수정하면, network가 놀라운 localization ability를 마지막 layer까지 보유할 수 있다는 것입니다.

 

이러한 수정은 네트워크가 매우 다양한 task를 위한 discriminative image region을 single forward-pass에서 쉽게 식별하도록 만들어줍니다. 

 

Figure 1(a)에서 보이는 것처럼, object categorization에 대해 학습된 CNN은 action classification을 위한 discriminative regions을 사람보다는 사람이 상호작용하고 있는 개체의 위치로 잘 찾고 있는 것을 볼 수 있습니다.

 

 

본 논문의 저자들의 접근법이 분명하게 간단함에도 불구하고, ILSVRC benchmark에 대한 weakly supervised object localization에 대해서 저자들의 최고 성능은 top-5 test error 기준 37.1%를 달성하였고, 이는 fully supervised AlexNet이 달성한 top-5 error 34.2%에 근접합니다.

 

추가적으로, 본 논문의 저자들은 제안하는 접근법을 통해 얻어지는 deep feature의 localizability가 generic classification, localization, 그리고 concept discovery를 위한 다른 recognition dataset에도 쉽게 사용될 수 있다는 것을 여러 가지 실험을 통해 검증합니다.

 

 

1.1 Related Work

 

선행 연구 부분은 생략합니다.

 

 

2. Class Activation Mapping

 

 

이번 section에서, 저자들은 CNNs에 있는 global average pooling을 사용해 class activation maps (CAM)를 만들어내는 절차에 대해서 설명합니다.

 

특정한 category에 대한 class activation map은 해당 이미지를 특정한 category로 식별하기 위해 CNN에 의해서 사용된 discriminative image region을 나타냅니다. 

 

Figure 3는 이에 대한 예시를 보여줍니다.

 

 

이러한 map을 만들어내는 절차는 Figure 2에 설명되어 있습니다.

 

 

 

저자들은 Network in Network와 GoogLeNet과 유사한 network architecture를 사용하였습니다.

 

즉, network는 주로 convolutional layer로 구성되고, 마지막 output layer의 바로 직전 convolutional feature map에 대해서 global average pooling을 수행하여 이를 desired output을 만들어내는 fully-connected layer를 위한 feature로써 사용합니다.

 

이 간단한 connectivity structure가 주어졌을 때, 저자들은 output layer의 weight를 convolutional feature maps에 대해서 다시 project 함으로써 image regions에 대한 importance를 확인할 수 있으며 저자들은 이 기법을 class activation mapping이라고 부릅니다.

 

 

Figure 2에서 볼 수 있듯이, global average pooling은 마지막 convolutional layer의 각 unit의 feature map의 spatial average를 만들어냅니다. 

 

이러한 값들의 weighted sum은 final output을 만들어내는 데 사용됩니다.

 

유사하게, 저자들은 class activation map을 얻기 위해서 last convolutional layer의 feature map의 weighted sum을 계산합니다.

 

저자들은 이를 softmax의 케이스에 대해 formally 하게 설명합니다. 이 기법은 다른 loss나 regression에 대해서도 적용할 수 있습니다.

 

 

 

(이 파트부터는 제가 직접 만든 그림과 함께 설명을 진행하겠습니다.)

 

 

주어진 이미지에 대해서, $f_k(x, y)$는 spatial location $(x, y)$에서의 last convolutional layer의 unit $k$의 activation을 나타냅니다. 

 

 

 

last convolutional layer의 크기를 (Batch, 3, 4, 4)라고 가정하겠습니다. (channel이 3, 가로 세로가 각각 4라고 가정)

 

여기서 channel이 3 이므로, $k$는 3까지 존재하게 됩니다.

 

그러고 3번째 channel의 (1, 1)의 위치는 $f_3(1,1)$로 표현될 수 있습니다.

 

 

 

그러고 나서, unit $k$에 대해, global average pooling을 수행한 결과인 $F^k$는 $\sum_{x, y}f_k(x, y)$입니다.

 

 

 

 

global average pooling을 하게 되면, 이전 $f_k$에서 4x4 짜리였던 데이터를 평균 내서 하나의 값으로 만들게 됩니다.

 

따라서 그림에서 $F^k$가 하나의 데이터로 표현이 되는 것이죠.

 

각 데이터들은 $F^1$, $F^2$, $F^3$가 됩니다.

 

 

따라서, 주어진 class $c$에 대해서, softmax의 input $S_c$는 $\sum_k w^c_kF_k$이며, $w^c_k$는 unit $k$가 class $c$에 대응되는 weight를 나타냅니다.

 

 

 

 

문제를 단순하게 하기 위해서, 하나의 이미지를 개와 고양이를 분류하는 이진 분류 문제로 가정하였습니다.

 

Global Average Pooling을 통해서 얻은 $F^k$를 weight와 곱해서 최종적으로 softmax의 input인 $S_c$를 얻게 됩니다.

 

이때, $S_1$ = $F^1 \times 0.3(w^1_1) + F^2 \times 0.5(w^1_2) + F^3 \times 0.1(w^1_3)$로 구할 수 있습니다.

 

 

 

동일한 방법을 적용하면, $S_2$ = $F^1 \times 0.4(w^2_1) + F^2 \times 0.6(w^2_2) + F^3 \times 0.8(w^2_3)$로 구할 수 있게 됩니다.

 

 

 

본질적으로, $w^c_k$는 class $c$에 대한 $F_k$의 importance를 나타냅니다.

 

 

최종적으로 class $c$에 대한 softmax output $P_c$는 ${exp(S_c)}/{\sum_c exp(S_c)}$로 나타내질 수 있습니다.

 

 

여기서 저자들은 bias term를 무시하고자 명시적으로 softmax의 input bias를 0으로 지정하는데, 이는 classification performance에 영향을 거의 주지 않기 때문이라고 합니다.

 

 

 

$F_k = \sum_{x, y}f_k(x, y)$를 class score $S_c$를 나타내는 식에 적용하면, 이렇게 표현할 수 있습니다.

 

 

 

저자들은 $M_c$를 class $c$에 대한 class activation map으로 정의하고, 각 spatial element는 다음과 같이 나타낼 수 있습니다.

 

 

 

위의 예시 그림을 통해서 표현해보면 다음과 같습니다.

 

 

따라서, $S_c = \sum_{x, y}M_c(x, y)$이며, $M_c(x, y)$는 직접적으로 image의 분류가 class $c$가 되도록 이끄는 spatial grid $(x, y)$에서의 activation의 importance를 나타냅니다.

 

 

 

직관적으로는, 이전 연구에 기반해서 본 논문의 저자들은 각 unit이 receptive field 내에서 어떤 visual pattern에 의해 활성화되는 것을 기대합니다.

 

따라서, $f_k$는 이러한 visual pattern의 존재에 대한 map이 됩니다.

 

Class activation map은 간단하게 다른 spatial locations에서 이러한 visual pattern의 존재에 대한 weighted linear sum이 됩니다.

 

Class activation map을 input image의 사이즈로 upsampling 해줌으로써, 특정한 category와 가장 관련이 있는 image regions을 식별할 수 있게 됩니다.

 

 

Figure 3에서는 위의 접근법을 사용해서 나온 CAM output의 여러 예시들을 보여줍니다.

 

다양한 클래스의 이미지들의 discriminative region이 강조된 것을 볼 수 있습니다.

 

Figure 4에서는, 하나의 이미지에 대해서 map을 만들 때 다른 classes $c$를 사용했을 때 CAM에서의 차이를 보여줍니다.

 

이를 통해서 다른 category에 대한 discriminative region이 주어진 이미지에 대해서 다르다는 것을 확인할 수 있습니다.

 

이는 저자들의 접근법이 기대한 대로 작동함을 나타냅니다. 

 

 

Global average pooling (GAP) vs Global max pooling (GMP)

 

 

Weakly supervised object localization에서 GMP을 사용한 이전 연구들을 고려해 볼 때, 저자들은 GAP와 GMP 사이의 직관적인 차이를 강조하는 것이 중요하다고 생각했다고 합니다.

 

저자들이 생각하기에는 하나의 discriminative part만을 식별하도록 만드는 GMP와 비교했을 때 GAP loss는 network가 object의 면적을 식별할 수 있도록 만든다고 합니다. 

 

이는 맵의 평균을 낼 때, 낮은 activation이 맵의 output을 감소시키므로 object의 모든 discriminative part를 찾아 최대화될 수 있기 때문이라고 합니다.

 

반면에 GMP의 경우는 가장 discriminative 한 image region을 제외하고는 모든 image region의 낮은 score가 max를 수행할 때 score에 영향을 주지 않습니다.

 

저자들은 이를 Section 3에서 ILSVRC dataset에 대해서 실험적으로 검증합니다.

 

GMP는 classification performance에 있어서는 GAP와 유사한 성능을 내지만, GAP는 localization에 있어서는 GMP보다 더 좋은 성능을 냅니다.

 

 

3. Weakly-supervised Object Localization

 

 

이번 section에서는 ILSVRC 2014 benchmark dataset에 대해서 CAM의 localization ability를 평가합니다.

 

모든 내용을 자세히는 다루지 않을 것이고, 간략하게 다루려고 합니다.

 

 

3.1 Setup

 

 

해당 실험에서는, AlexNet, VGGnet, GoogLeNet을 사용하여 실험을 진행합니다.

 

이 3개의 network는 원래 fully-connected layer가 있지만, 이를 제거하고 GAP로 대체하여 실험을 진행합니다. 

 

fully-connected layer가 network parameter의 수를 상당히 많이 감소시키므로, classification performance의 drop을 가져오게 됩니다.

 

 

그리고 저자들은 network의 localization ability가 GAP 이전의 last convolutional layer가 더 큰 spatial resolution을 가질 때 향상된다는 사실을 발견하였으며, 이를 mapping resolution이라고 부른다고 합니다.

 

 

Classification에 대해서는 original AlexNet, VGGnet, GoogLeNet, Network in Network에 대한 결과를 제공합니다.

 

Localization에 대해서는 original GoogLeNet, NIN, 그리고 backpropagation을 이용한 결과를 비교합니다.

 

추가적으로, max pooling과 average pooling을 비교하기 위해, global max pooling으로 학습된 GoogLeNet의 결과도 제공합니다. (GoogLeNet-GMP)

 

 

3.2 Results

 

 

Classification

 

 

Table 1에서 classification performance의 비교를 보여줍니다.

 

 

다양한 network에서 layer를 제거하는 것에 의해 약간의 성능 하락이 보이고 있습니다.

 

AlexNet이 fully-connected layer의 제거에 의해 가장 크게 영향을 받았는데, 이를 보상해주기 위해 두 개의 convolutional layer를 추가하여 AlexNet*-GAP network로 실험했다고 합니다.

 

그리고 GoogLeNet-GMP와 GoogLeNet-GAP는 유사한 성능을 보이고 있는데, 이는 저자들이 예측한 대로 나오고 있음을 알 수 있는 부분입니다.

 

 

Localization

 

 

Localization을 수행하려면, bounding box를 만들어야 하는데요.

 

저자들은 CAM에서 bounding box를 만들기 위해서, thresholding 하는 방법을 사용했습니다.

 

CAM의 max value의 20%보다 높은 값들을 가지는 구역만 잘라낸 뒤에, 이를 포함할 수 있는 가장 큰 bounding box를 만드는 방법입니다.

 

Figure 6(a)에는 이 방법을 사용하여 만들어낸 bounding box의 예시들을 볼 수 있습니다.

 

 

그리고 ILSVRC validation set에서의 localization performance는 Table 2에 나와있고, 예시 output은 Figure 5에 나와있습니다.

 

 

추가적으로 저자들이 제안한 CAM 방법과 이미 존재하는 weakly-supervised, fully-supervised CNN method와 비교하기 위해서, GoogLeNet-GAP을 ILSVRC test set에 대해 성능 실험을 수행했습니다. 

 

성능 비교와 관련된 자료는 Table 3에 나와있습니다.

 

fully-supervised AlexNet과 weakly-supervised GoogLeNet의 성능이 거의 비슷하네요.

 

 

4. Deep Features for Generic Localization

 

 

CNN의 higher-level layer들로부터 나오는 response는 다양한 이미지 데이터셋에서 SOTA 성능을 내게 해주는 매우 효과적인 generic feature라는 것이 알려져 왔습니다.

 

해당 section에서는, 저자들이 제안한 GAP CNNs으로부터 학습된 feature가 generic feature로써 잘 작동하는지를 검증합니다.

 

 

4.1 Fine-grained Recognition

 

 

 

해당 section에서는, 200개의 새 품종을 식별하는 CUB-200-2011 dataset을 이용해 실험을 진행합니다.

 

section 3.2에서 사용한 것과 동일하게, thresholding을 사용하여서 bounding box를 만들어낸 모습입니다.

 

 

4.2 Pattern Discovery

 

Discovering informative objects in the scenes

 

SUN dataset를 이용해 실험을 진행하였으며, 다음 Figure 9는 예측된 scene category에 대한 CAM과 높은 CAM activation이 가장 빈번하게 겹치는 top 6 objects를 나타낸 것입니다. 

 

 

 

Concept localization in weakly labeled images

 

 

concept detector를 학습하고, 이미지에서 concept의 위치를 확인하기 위해 CAM technique을 적용하였습니다.

 

Figure 10은 top ranked image와 CAM을 보여주는 그림입니다.

 

 

나머지 실험들도 더 있었지만, 결국 CAM의 localization ability를 검증하는 실험이어서 생략하도록 하겠습니다.

 

 

6. Conclusion

 

 

본 논문에서, 저자들은 global average pooling을 가지고 있는 CNN 모델에 대해 Class Activation Mapping이라고 불리는 일반적인 기술을 제안합니다.

 

이는 classification에 학습된 CNN이 어떠한 bounding box annotation 없이 object localization을 수행할 수 있도록 만들어줍니다.

 

Class activation map은 주어진 이미지에 대해서 예측된 class score를 시각화하게 해 주고, CNN에 의해서 detected 된 discriminative object part를 강조합니다.

 

저자들은 CAM을 ILSVRC benchmark에 대해 실험을 진행했으며, global average pooling CNN이 정확한 object localization을 수행할 수 있음을 검증하였습니다.

 

추가적으로 저자들은 CAM localization technique이 다른 visual recognition task에 대해 일반화할 수 있다는 사실을 검증하였습니다.

 

즉, CAM은 다른 연구자들이 CNN에 의해 사용되는 discrimination의 기초를 이해하는 데 있어서 도움을 줄 수 있는 generic localizable deep feature를 만들어냅니다.

 

 

 

여기까지 CAM 논문에 대한 review를 해보았습니다.

 

Deep Learning의 경우, black-box 방법이기 때문에 왜 이러한 의사결정을 하는지 판단하기가 어렵습니다.

 

하지만 CAM을 이용하게 되면, 과연 어느 부분을 보고 이러한 의사결정을 했는지 파악할 수 있다는 점에서 딥러닝의 해석 가능성과 연관된 논문이라고 생각했고, 그 점에서 흥미롭다고 판단되어 review를 진행하게 되었습니다.

 

다음 포스팅에서는 해당 논문에 대한 code review를 진행하겠습니다.

 

감사합니다.

 

 

안녕하세요.

 

이번 글에서는 저번 글에서 리뷰했던 Wide Residual Networks(WRN)의 code review를 진행해보겠습니다.

 

ResNet과 유사한 형태이기 때문에, ResNet를 이해하셨다면 조금 더 쉽게 보실 수 있을 것 같습니다.

 

해당 논문에 대한 paper review는 다음 주소에서 확인하실 수 있습니다.

 

cumulu-s.tistory.com/35

 

7. Wide Residual Networks(WRN) - paper review

안녕하세요. 오늘은 ResNet을 발전시켜서 만들어진 Wide Residual Networks(WRN)에 대한 내용을 정리해보려고 합니다. 논문 주소: arxiv.org/abs/1605.07146 Wide Residual Networks Deep residual networks were..

cumulu-s.tistory.com

 

시작해보겠습니다.

 

 

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
from tqdm.auto import tqdm
import torchvision
import torchvision.transforms as transforms
import datetime
import os
from torch.utils.tensorboard import SummaryWriter
import shutil

먼저, 필요한 library를 가져옵니다.

 

 

device = 'cuda' if torch.cuda.is_available() else 'cpu'

# Data
print('==> Preparing data..')
transform_train = transforms.Compose([
    transforms.RandomCrop(32, padding=4),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
])

transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
])

trainset = torchvision.datasets.CIFAR10(
    root='/content/drive/MyDrive/CIFAR10', train=True, download=True, transform=transform_train)
trainloader = torch.utils.data.DataLoader(
    trainset, batch_size=128, shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(
    root='/content/drive/MyDrive/CIFAR10', train=False, download=True, transform=transform_test)
testloader = torch.utils.data.DataLoader(
    testset, batch_size=100, shuffle=False, num_workers=2)

current_time = datetime.datetime.now() + datetime.timedelta(hours= 9)
current_time = current_time.strftime('%Y-%m-%d-%H:%M')

saved_loc = os.path.join('/content/drive/MyDrive/WRN_Result', current_time)
if os.path.exists(saved_loc):
    shutil.rmtree(saved_loc)
os.mkdir(saved_loc)

print("결과 저장 위치: ", saved_loc)

writer = SummaryWriter(saved_loc)

데이터를 들고 올 준비를 해줍니다.

 

 

class BasicBlock(nn.Module):
    def __init__(self, in_planes, out_planes, stride, dropRate=0.0):
        super(BasicBlock, self).__init__()
        self.bn1 = nn.BatchNorm2d(in_planes)
        self.relu1 = nn.ReLU(inplace=True)
        self.conv1 = nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
                               padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_planes)
        self.relu2 = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_planes, out_planes, kernel_size=3, stride=1,
                               padding=1, bias=False)
        self.droprate = dropRate
        self.equalInOut = (in_planes == out_planes)
        self.convShortcut = (not self.equalInOut) and nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride,
                               padding=0, bias=False) or None
    def forward(self, x):
        if not self.equalInOut:
            x = self.relu1(self.bn1(x))
        else:
            out = self.relu1(self.bn1(x))
        out = self.relu2(self.bn2(self.conv1(out if self.equalInOut else x)))
        if self.droprate > 0:
            out = F.dropout(out, p=self.droprate, training=self.training)
        out = self.conv2(out)
        return torch.add(x if self.equalInOut else self.convShortcut(x), out)

class NetworkBlock(nn.Module):
    def __init__(self, nb_layers, in_planes, out_planes, block, stride, dropRate=0.0):
        super(NetworkBlock, self).__init__()
        self.layer = self._make_layer(block, in_planes, out_planes, nb_layers, stride, dropRate)
    def _make_layer(self, block, in_planes, out_planes, nb_layers, stride, dropRate):
        layers = []
        for i in range(int(nb_layers)):
            layers.append(block(i == 0 and in_planes or out_planes, out_planes, i == 0 and stride or 1, dropRate))
        return nn.Sequential(*layers)
    def forward(self, x):
        return self.layer(x)

class WideResNet(nn.Module):
    def __init__(self, depth, num_classes, widen_factor=1, dropRate=0.0):
        super(WideResNet, self).__init__()
        nChannels = [16, 16*widen_factor, 32*widen_factor, 64*widen_factor]
        assert((depth - 4) % 6 == 0) # depth must be 6n + 4
        n = (depth - 4) / 6

        # 1st conv before any network block
        self.conv1 = nn.Conv2d(3, nChannels[0], kernel_size=3, stride=1,
                               padding=1, bias=False)
        # 1st block
        self.block1 = NetworkBlock(n, nChannels[0], nChannels[1], BasicBlock, 1, dropRate)
        # 2nd block
        self.block2 = NetworkBlock(n, nChannels[1], nChannels[2], BasicBlock, 2, dropRate)
        # 3rd block
        self.block3 = NetworkBlock(n, nChannels[2], nChannels[3], BasicBlock, 2, dropRate)
        # global average pooling and classifier
        self.bn1 = nn.BatchNorm2d(nChannels[3])
        self.relu = nn.ReLU(inplace=True)
        self.fc = nn.Linear(nChannels[3], num_classes)
        self.nChannels = nChannels[3]


        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, nn.BatchNorm2d):
                m.weight.data.fill_(1)
                m.bias.data.zero_()
            elif isinstance(m, nn.Linear):
                m.bias.data.zero_()


    def forward(self, x):
        out = self.conv1(x)
        out = self.block1(out)
        out = self.block2(out)
        out = self.block3(out)
        out = self.relu(self.bn1(out))
        out = F.avg_pool2d(out, 8, 1, 0)
        out = out.view(-1, self.nChannels)
        return self.fc(out)

 

WideResNet이라는 이름으로 WRN를 나타내는 class를 만들어줍니다. 

 

input으로는 depth, class 개수, widen_factor, dropout rate를 받게 됩니다.

 

Channel의 개수는 16, 16*k, 32*k, 64*k로 구성되며, k는 이전 논문 review에서도 언급되었듯이 widening factor를 나타냅니다.

 

이를 통해 기존 original residual network보다 더 넓은 형태의 residual network를 구성할 수 있습니다.

 

depth의 경우는 반드시 6n + 4의 값이 되어야 하는데, 이는 다음 표를 보고 이해해볼 수 있습니다.

 

먼저, conv1에서 1개, 그리고 conv2, conv3, conv4에서 shortcut connection 형태로 layer가 들어가게 되어 기본적으로 4개의 layer가 있어야 합니다.

 

그리고 6n이 되는 이유는, conv2에서 3x3 짜리 2개, conv3에서 3x3짜리 2개, conv4에서 3x3짜리 2개로 총 6개가 기본적인 setting이므로, 이 setting보다 몇 배 더 많은 layer를 구성할 것인지를 결정하기 위해 6n가 되게 됩니다. 

 

만약 layer의 총개수가 16이라면, 6*2 + 4가 되며, 이는 conv2 block 내에서의 layer가 4개, conv3 block 내에서의 layer가 4개, conv4 block 내에서의 layer가 4개가 됩니다. 

 

즉, n이라는 것은 conv block 내에서 3x3 conv layer의 개수에 곱해지는 계수라고 생각할 수 있습니다.

 

 

 

위의 Table 1의 structure가 코드로 구현되어 있음을 확인할 수 있는데요.

 

WRN의 전체적인 흐름부터 살펴보겠습니다.

 

    def forward(self, x):
        out = self.conv1(x)
        out = self.block1(out)
        out = self.block2(out)
        out = self.block3(out)
        out = self.relu(self.bn1(out))
        out = F.avg_pool2d(out, 8, 1, 0)
        out = out.view(-1, self.nChannels)
        return self.fc(out)

(WideResNet에서 이쪽 부분을 확인해주시면 됩니다!)

 

 

먼저 self.conv1은 3x3 conv이며, 16 channel이 output이 되도록 연산이 되게 됩니다.

 

self.block1, self.block2, self.block3는 위 Table 1에서 conv2, conv3, conv4에 대응됩니다.

 

self.block에 대해서는 조금 있다가 자세히 살펴보도록 할 예정입니다.

 

self.block3까지 지난 결과를 BN과 ReLU를 통과시키고, average pooling을 적용해줍니다. 

 

그리고 이를 [batch_size, 64*widen_factor]의 shape로 만들어주고, 이를 마지막 fully connected layer에 연결하여

 

dataset의 class 각각에 대한 probability를 정답으로 내게 됩니다.

 

 

 

이전에 paper review를 진행할 때, WRN의 contribution 중 하나가 바로 batch normalization, activation, convolutional layer의 순서에 대한 연구를 진행한 것이었고, 따라서 어떤 순서로 연산할지 변경되었다는 얘기를 했었습니다.

 

그렇다면, 여기서 ResNet과 Wide Residual Network는 어떻게 다른지 한번 살펴보겠습니다.

 

class ResNet(nn.Module):
    def __init__(self, block, num_blocks, num_classes=10):
        super(ResNet, self).__init__()
        self.in_planes = 64

        self.conv1 = nn.Conv2d(3, 64, kernel_size=3,
                               stride=1, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)
        self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
        self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
        self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
        self.linear = nn.Linear(512*block.expansion, num_classes)

    def _make_layer(self, block, planes, num_blocks, stride):
        strides = [stride] + [1]*(num_blocks-1)
        layers = []
        for stride in strides:
            layers.append(block(self.in_planes, planes, stride))
            self.in_planes = planes * block.expansion
        return nn.Sequential(*layers)

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = F.avg_pool2d(out, 4)
        out = out.view(out.size(0), -1)
        out = self.linear(out)
        return out

 

이전 ResNet code review를 할 때 사용했던 코드입니다.

 

ResNet에서는 먼저 input 데이터를 받아 conv1 -> BN -> relu를 거치고 conv block을 거친 후, average pooling을 하고, 이를 fully connected layer에 연결하는 순서로 이루어집니다.

 

하지만, Wide residual network는 input 데이터를 받아 conv1을 거치고, conv block을 거친 후, BN -> relu를 거치고 average pooling을 한 후 이를 fully connected layer에 연결하는 순서로 이루어집니다.

 

전체적인 흐름상 batch normalization과 ReLU의 적용 위치가 바뀌었네요.

 

그리고 conv block이 ResNet에서는 4개였으나, Wide residual network에서는 3개라는 점도 하나의 차이가 될 수 있겠네요.

 

conv block 1개당 stride = 2가 적용되므로, 따라서 ResNet은 conv block이 모두 진행되었을 때 resolution이 4x4지만, wide residual network는 resolution이 8x8이 됩니다.

 

 

 

다음으로는 conv block에 대해서 세부적으로 살펴보겠습니다.

 

self.block1 = NetworkBlock(n, nChannels[0], nChannels[1], BasicBlock, 1, dropRate)

 

먼저 첫 번째 conv block을 살펴봅니다.

 

간단한 예시를 들기 위해서, n = 2 / nChannels[0] = 16 / nChannels[1] = 64 / dropRate = 0.3으로 지정한다고 가정하겠습니다.

 

class NetworkBlock(nn.Module):
    def __init__(self, nb_layers, in_planes, out_planes, block, stride, dropRate=0.0):
        super(NetworkBlock, self).__init__()
        self.layer = self._make_layer(block, in_planes, out_planes, nb_layers, stride, dropRate)
    def _make_layer(self, block, in_planes, out_planes, nb_layers, stride, dropRate):
        layers = []
        for i in range(int(nb_layers)):
            layers.append(block(i == 0 and in_planes or out_planes, out_planes, i == 0 and stride or 1, dropRate))
        return nn.Sequential(*layers)
    def forward(self, x):
        return self.layer(x)

 

따라서, 우리가 만들어낸 것은 NetworkBlock(nb_layers = 2, in_planes = 16, out_planes = 64, block = BasicBlock, stride = 1, dropRate= 0.3)이 됩니다.

 

이를 통해서 만들어지는 것은 다음과 같습니다.

 

def _make_layer(self, block = BasicBlock, in_planes=16, out_planes=64, nb_layers=2, stride=1, dropRate=0.3):
        layers = []
        for i in range(int(nb_layers)):
            layers.append(block(i == 0 and in_planes or out_planes, out_planes, i == 0 and stride or 1, dropRate))
        return nn.Sequential(*layers)

 

layer의 개수만큼 for문을 돌게 되고, 이를 통해서 layers = []에 block이 append 되는 구조입니다.

 

nb_layers = 2이므로, block이 2개 쌓이게 되겠죠?

 

여기서 조금 생소한 코드가 등장하는데요, A and B or C 형태의 문법이 사용되었습니다.

 

이는 B if A else C와 똑같은 의미입니다.

 

즉, BasicBlock(in_planes if i == 0 else out_planes, out_planes, stride if i == 0 else 1, dropRate)가 만들어집니다.

 

layers = [BasicBlock(16, 64, 1, 0.3), BasicBlock(64, 64, 1, 0.3)]가 될 것입니다.

 

다음으로는 BasicBlock에 대해서 살펴봐야겠죠?

 

class BasicBlock(nn.Module):
    def __init__(self, in_planes, out_planes, stride, dropRate=0.0):
        super(BasicBlock, self).__init__()
        self.bn1 = nn.BatchNorm2d(in_planes)
        self.relu1 = nn.ReLU(inplace=True)
        self.conv1 = nn.Conv2d(in_planes, out_planes, kernel_size=3, stride=stride,
                               padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_planes)
        self.relu2 = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_planes, out_planes, kernel_size=3, stride=1,
                               padding=1, bias=False)
        self.droprate = dropRate
        self.equalInOut = (in_planes == out_planes)
        self.convShortcut = (not self.equalInOut) and nn.Conv2d(in_planes, out_planes, kernel_size=1, stride=stride,
                               padding=0, bias=False) or None
    def forward(self, x):
        if not self.equalInOut:
            x = self.relu1(self.bn1(x))
        else:
            out = self.relu1(self.bn1(x))
        out = self.relu2(self.bn2(self.conv1(out if self.equalInOut else x)))
        if self.droprate > 0:
            out = F.dropout(out, p=self.droprate, training=self.training)
        out = self.conv2(out)
        return torch.add(x if self.equalInOut else self.convShortcut(x), out)

 

BasicBlock(16, 64, 1, 0.3)이라고 가정하고 생각해보겠습니다.

 

여기서 먼저 따져봐야 할 점은 input의 shape와 output의 shape가 같은지입니다. 이를 확인한 후, self.equalInOut이라는 변수로 boolean 값으로 저장합니다.

 

BasicBlock(16, 64, 1, 0.3)이라면 input의 shape와 output의 shape가 다른 경우이므로, self.equalInOut은 False가 됩니다.

 

self.convShortcut의 경우는 self.equalInOut이 False이면 nn.conv2d를 적용하고, True이면 None이 됩니다.

 

다음으로는 forward 부분을 보면 됩니다.

 

if not self.equalInOut이므로, 이 if문에 들어가게 됩니다.

 

따라서 x = self.relu1(self.bn1(x))이 됩니다.

 

그러고 나서 out = self.relu2(self.bn2(self.conv1(x)))이 되고

 

droprate = 0.3이므로, 여기에 dropout을 적용하여 out = F.dropout(out, p = self.droprate, training = self.training)을 적용합니다.

 

training = self.training을 적용하는 이유는, 현재 모델이 train모드인지 eval 모드인지에 따라서 dropout을 사용할지 아닐지를 결정하게 됩니다.

 

왜냐하면 학습이 된 상태에서 추론을 하는 경우에는 dropout을 사용하지 않아야 하기 때문이죠.

 

그리고 나서 out = self.conv2(out)을 해주고

 

torch.add(self.convShortcut(x))을 해준 것을 return 해줍니다.

 

이를 그림으로 한번 그려보자면, 다음과 같습니다.

 

 

k는 kernel size이며 s는 stride, p는 padding을 나타냅니다.

 

본 그림을 통해서 BasicBlock의 구동 방식을 이해해볼 수 있을 것이라고 생각이 들고요.

 

만약 stride = 2인 경우는 self.conv1에서 s = 2로 변하고, 그 이후로 가로와 세로의 길이가 /2로 줄어듭니다.

 

그리고 input과 output의 channel 수가 동일한 경우는 위의 그림에서 self.conv1 = nn.Conv2d(16, 64, ~)을 통과하면서 channel수가 변하는 것이 아닌, 그대로 channel 수가 유지되게 됩니다.

 

self.convShortcut 또한 필요가 없어지기 때문에, 이는 None이 되고 위의 def forward에서 return 쪽을 확인해보시게 되면, input인 x를 그대로 torch.add에 넣어주는 것을 확인할 수 있습니다. 

 

여기까지 WRN의 모델에 대한 설명은 마무리가 되었습니다.

 

 

net = WideResNet(40, 10, 2, 0.3) # WRN-40-2

net = net.to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.1,
                      momentum=0.9, weight_decay=0.0005)
scheduler = optim.lr_scheduler.MultiStepLR(optimizer, milestones = [60, 120, 160], gamma = 0.2)

best_acc = 0

 

WideResNet의 첫 번째 인자는 layer의 수, 두 번째 인자는 class의 수, 세 번째 인자는 widening factor, 네 번째 인자는 dropout rate입니다.

 

optimizer는 논문에 나온 그대로 lr = 0.1, momentum = 0.9, weight_decay = 0.0005를 적용하였으며

 

scheduler는 MultiStepLR을 이용하여 구현하였습니다. milestones을 통해서 어떤 지점에서 scheduler를 적용할지 입력하고, gamma는 얼마큼 감소시킬지 값을 알려줍니다.

 

처음 learning rate는 0.1로, 그리고 60, 120, 160 에폭 일 때 각각 0.2씩 감소하라는 내용을 그대로 반영하였습니다.

 

 

def train(epoch):
    print('\nEpoch: %d' % epoch)
    net.train()
    train_loss = 0
    correct = 0
    total = 0
    batch_count = 0
    for batch_idx, (inputs, targets) in enumerate(trainloader):
        inputs, targets = inputs.to(device), targets.to(device)
        optimizer.zero_grad()
        outputs = net(inputs)
        loss = criterion(outputs, targets)
        loss.backward()
        optimizer.step()

        train_loss += loss.item()
        _, predicted = outputs.max(1)
        total += targets.size(0)
        correct += predicted.eq(targets).sum().item()
        batch_count += 1

    print("Train Loss : {:.3f} | Train Acc: {:.3f}".format(train_loss / batch_count, 100.*correct/total))
    final_loss = train_loss / batch_count
    final_acc = 100.*correct / total
    return final_loss, final_acc

 

inputs로 이미지 데이터를, targets로 label을 받아오고

 

이미지를 신경망에 투입하여 outputs을 만들고

 

outputs과 targets 과의 cross entropy를 계산해서 loss로 만듭니다.

 

이 값은 backpropagation 시키고, train_loss에 더해줍니다.

 

output.max(1)을 적용하면, row 기준으로 output에서 가장 높은 값을 가지는 index와 예측값을 뽑아줍니다.

 

예측값은 필요 없으므로 _로 처리하고, 예측된 index를 predicted로 저장합니다.

 

그리고 predicted.eq(targets)를 통해서 label과 예측된 label이 몇 개나 같은지를 boolean 값으로 계산합니다.

 

그리고 sum()을 해주면, 배치 사이즈 기준으로 몇 개나 label와 일치하는지를 값으로 계산할 수 있습니다. 

 

 

def test(epoch):
    global best_acc
    net.eval()
    test_loss = 0
    correct = 0
    total = 0
    batch_count = 0
    with torch.no_grad():
        for batch_idx, (inputs, targets) in enumerate(testloader):
            inputs, targets = inputs.to(device), targets.to(device)
            outputs = net(inputs)
            loss = criterion(outputs, targets)

            test_loss += loss.item()
            _, predicted = outputs.max(1)
            total += targets.size(0)
            correct += predicted.eq(targets).sum().item()
            batch_count += 1


    print("Test Loss : {:.3f} | Test Acc: {:.3f}".format(test_loss / batch_count, 100.*correct/total))

    # Save checkpoint.
    acc = 100.*correct/total
    if acc > best_acc:
        print('Saving..')
        state = {
            'net': net.state_dict(),
            'acc': acc,
            'epoch': epoch,
        }
        if not os.path.isdir(os.path.join(saved_loc, 'checkpoint')):
            os.mkdir(os.path.join(saved_loc, 'checkpoint'))
        torch.save(state, os.path.join(saved_loc, 'checkpoint/ckpt.pth'))
        best_acc = acc
    
    final_loss = test_loss / batch_count
    return final_loss, acc

 

위의 내용은 동일하니 설명을 생략하고,

 

accuracy의 경우 best_acc를 계속 갱신하면서, best_acc보다 더 높은 accuracy가 나오면 이를 best_acc로 저장하면서 동시에 모델과 accuracy 값, 그 당시 epoch을 저장합니다.

 

이를 통해서 test 기준으로 가장 높게 나온 네트워크의 가중치와 accuracy 값을 저장할 수 있게 됩니다.

 

 

for epoch in tqdm(range(200)):
    train_loss, train_acc = train(epoch)
    test_loss, test_acc = test(epoch)

    writer.add_scalars('Loss', {"Train Loss" : train_loss, "Test Loss" : test_loss}, epoch)
    writer.add_scalars('Accuracy', {"Train acc" : train_acc, "test acc" : test_acc}, epoch)
    
    scheduler.step()

writer.close()

학습은 200 epochs 동안 진행하고, writer.add_scalars는 tensorboard에 값을 저장하는 코드입니다.

 

이를 통해서 train_loss, test_loss, train_acc, test_acc 그래프를 그릴 수 있게 됩니다.

 

그리고 앞에서 scheduler를 지정했기 때문에, 1 epoch이 지날 때마다 scheduler.step()을 적용하여 원하는 지점에서 learning rate가 변하도록 지정해줍니다. 

 

 

 

이를 통해 얻게 되는 Accuracy 그래프는 다음과 같습니다.

 

 

 

중간에 learning rate가 변했다 보니, 약간 각진 그래프가 나오게 되네요.

 

 

 

loss graph도 마찬가지로 잘 수렴하는 모습을 보여주고 있습니다.

 

 

 

여기까지 WRN의 code review를 해보았습니다.

 

해당 글에서 다룬 모든 코드는 제 Github에서 확인하실 수 있습니다.

 

github.com/PeterKim1/paper_code_review

 

PeterKim1/paper_code_review

paper review with codes. Contribute to PeterKim1/paper_code_review development by creating an account on GitHub.

github.com

 

다음에는 또 새로운 논문으로 찾아오도록 하겠습니다.

 

감사합니다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

안녕하세요.

 

 

오늘은 ResNet을 발전시켜서 만들어진 Wide Residual Networks(WRN)에 대한 내용을 정리해보려고 합니다.

 

 

논문 주소: arxiv.org/abs/1605.07146

 

Wide Residual Networks

Deep residual networks were shown to be able to scale up to thousands of layers and still have improving performance. However, each fraction of a percent of improved accuracy costs nearly doubling the number of layers, and so training very deep residual ne

arxiv.org

 

그럼 시작해보겠습니다.

 

 

Abstract

 

 

Deep Residual networks는 수천 개의 layer까지 scale up 할 수 있음을 보여주었고, 향상된 성능을 보여준 모델입니다.

 

하지만, 정확도를 향상하기 위해서는 layer의 수를 많이 늘려야 했고, 매우 깊은 residual network를 학습시키는 것은 감소된 feature를 재사용하는 문제를 가지고 있으며, 이는 학습을 매우 느리게 만든다고 합니다.

 

이 문제를 해결하기 위해, 본 논문에서 저자들은 residual network의 width는 증가시키고 depth는 감소시키는 새로운 architecture를 기반으로 ResNet block의 architecture에 대한 상세한 실험 연구를 수행합니다.

 

저자들은 그 결과로 나온 network structure를 wide residual network (WRNs)라고 부르고, 흔하게 사용되는 얇고 매우 깊은 네트워크에 비해서 훨씬 더 우월한 성능을 내는 것을 보여줍니다.

 

 

1. Introduction

 

 

Convolutional neural network는 지난 몇 년간 층의 수를 점차적으로 늘려왔습니다.

 

AlexNet에서부터 시작해서, VGG, Inception, Residual network까지 층의 수를 늘리는 것을 통해 image recognition task에서 많은 성능 향상을 가져왔습니다.

 

하지만, deep neural network를 학습시키는 것은 exploding/vanishing gradient와 degradation을 포함해 여러 가지 어려움을 가지고 있습니다. 

 

Well-designed initialization strategies, better optimizers, skip connections, knowledge transfer and layer-wise training과 같은 다양한 기법들이 deeper neural network를 학습시킬 수 있도록 하기 위해서 제안되어 왔습니다.

 

가장 최근의 residual network는 다양한 benchmark에서 SOTA를 달성하였으며, 이와 관련된 후속 연구에서는 residual network에서의 activation의 순서에 대해서 연구가 진행되었습니다.

 

 

지금까지, residual network의 연구는 주로 ResNet block 내에서의 activation의 순서와 residual network의 depth에 대해서만 이루어졌습니다.

 

본 논문에서, 저자들은 이 point를 넘어서는 실험적인 연구를 수행하려고 시도했습니다.

 

본 논문에서의 저자들의 목표는 ResNet blocks의 훨씬 더 풍부한 network architecture를 탐색하고, activations의 순서 외에 다른 몇 가지 측면들이 성능에 어떻게 영향을 미치는지 철저히 검토하는 것입니다.

 

 

Width vs depth in residual networks

 

 

Residual network의 저자들은 residual network의 depth를 증가시키고 더 적은 parameter를 가지게 하기 위해서 network를 가능한 얇게 만드려고 시도했으며, 심지어 ResNet block을 더욱 얇게 만들기 위해서 bottleneck block을 도입하였습니다.

 

그러나, 본 논문의 저자들은 매우 깊은 네트워크를 훈련시킬 수 있는 identity mapping을 가진 residual block이 동시에 residual network의 약점임을 주목했다고 합니다.

 

Gradient가 네트워크를 통해서 흐르기 때문에 residual block weight를 거치도록 강제할 수 있는 것이 없으며, 훈련 중에 어떤 것도 학습하지 않을 수 있으므로 오직 몇 개의 블록만 유용한 표현을 학습하거나, 혹은 많은 블록들은 최종 목표에 적은 기여를 하는 매우 작은 정보만 공유할 수 있다고 합니다.

 

이 문제는 diminishing feature reuse로 이전 선행연구에서 표현되었다고 하네요. 

 

(제가 생각하기에는, layer가 많다 보면 backpropagation을 통해서 gradient가 layer 각각에 전달되는 과정에서 모든 layer가 모두 유용한 representation을 얻지 못할 수도 있다 라는 얘기를 하는 것 같습니다. 우리 몸으로 따지자면 심장에 가까운 부위들은 산소를 많이 가지고 있는 혈액을 공급받고, 심장에서 먼 부위들은 산소가 적은 혈액을 공급받는 느낌..?) 

 

 

본 논문의 저자들은 network의 depth를 증가시키는 것과 비교했을 때 ResNet block의 widening이 적절하게 이루어진다면 성능을 향상하는 더욱 효과적인 방법을 제공하는 것임을 증명합니다.

 

특히, 저자들은 이전 연구에 비해서 상당히 향상되고, 50배 더 적은 layer를 가지며 2배 더 빠른 wider deep residual network를 제시하였는데, 이를 wide residual networks라고 부릅니다.

 

예를 들어, 본 논문의 저자들이 제시한 wide 16-layer deep network는 1000-layer thin deep network와 유사한 accuracy를 가지며, 몇 배 더 빠르게 학습되면서 비슷한 수의 parameter를 가집니다. 

 

따라서, 이러한 유형의 실험들은 deep residual network의 주요한 힘이 residual block에 있으며, depth의 효과는 추가적임을 나타냅니다. 

 

 

Use of dropout in ResNet blocks

 

 

Dropout은 'Dropout: A simple way to prevent neural networks from overfitting'이라는 논문에서 처음으로 도입되었고, 많은 성공적인 architecture에 채택되었습니다.

 

이는 대부분 많은 수의 parameter를 가지고 있는 top layer에 적용되었고, 이는 feature coadaptation과 overfitting을 방지하기 위함입니다.

 

Dropout은 주로 batch normalization에 의해서 대체되었는데, 이는 batch normalization도 regularizer의 역할을 수행하기 때문이며, 실험적으로 batch normalization을 가지는 network가 dropout을 가진 network에 비해서 더 나은 accuracy를 가진다는 것이 보였기 때문입니다. 

 

 

본 논문에서는, residual block의 widening이 parameter의 수를 증가시키는 결과를 가져오기 때문에, 학습을 규제하고 overfitting을 막는 dropout의 효과를 연구하였다고 합니다.

 

이전에는, residual network에서의 dropout은 block의 identity part에 투입되었으며 부정적인 효과를 보였습니다.

 

그 대신에, 본 논문에서는 dropout을 convolutional layers들 사이에 투입시킵니다. 

 

Wide residual network에 대한 실험적인 결과는 dropout이 일관성 있게 성능을 향상한다는 것을 보여주며, 심지어 새로운 SOTA result를 만들어내는 것을 보였습니다.

 

 

지금까지의 내용을 요약하자면, 본 논문의 contribution은 다음과 같습니다.

 

  • ResNet block structure에 대한 여러가지 중요한 측면을 철저히 조사하는 residual network architecture의 상세한 실험적인 연구를 제시합니다.
  • Residual network가 상당히 향상된 성능을 낼 수 있도록 만들어주는 ResNet block에 대한 새로운 widened architecture를 제안합니다.
  • 학습이 이루어지는 동안에 network를 적절하게 규제하고 overfitting을 회피하기 위해 deep residual network 내에서 dropout을 활용하는 새로운 방법을 제안합니다.
  • 마지막으로, 새롭게 제안된 ResNet architecture가 여러 가지 dataset에서 향상된 accuracy와 속도를 보여주며 SOTA result를 달성하는 것을 보입니다.

 

2. Wide residual networks

 

 

Identity mapping을 가지는 residual block은 다음과 같은 식으로 표현될 수 있습니다.

 

 

$x_l$과 $x_{l+1}$은 network에서 $l$번째 unit의 input과 output을 의미하며, $\mathcal{F}$은 residual function이고 $\mathcal{W}_l$은 block의 parameter를 나타냅니다. 

 

Residual network는 연속적으로 쌓여진 residual block으로 구성됩니다.

 

 

 

Residual network는 두 가지 형태의 block으로 구성되는데요.

 

 

  • Basic - 연속된 3x3 convolution으로 구성되며, batch normalization과 ReLU가 앞선 convolution에 적용되는 구조
  • Bottleneck - 하나의 3x3 convolution은 차원을 감소하고 증가시키는 1x1 convolution layer에 둘러싸여 있는 구조

 

Original architecture와 비교해서, residual block에서의 batch normalization, activation and convolution의 순서는 conv-BN-ReLU에서 BN-ReLU-conv로 변경합니다.

 

후자가 더 빠르게 학습되고 더 나은 결과를 가져오기 때문에, 본 논문에서는 original version을 고려하지 않습니다.

 

Bottleneck block은 layer의 수를 늘리기 위해서 block의 연산량을 감소시키고자 사용되었는데, 본 논문에서 저자들은 widening의 효과를 연구하길 원하는 것이므로 networks를 얇게 만드는 데 사용되는 bottleneck 구조는 고려하지 않습니다.

 

 

Residual block의 representational power를 증가시키는 3가지 근본적인 간단한 방법이 있습니다.

 

  • Block 당 더 많은 convolutional layer를 추가하는 방법
  • 더 많은 feature planes를 추가하여 convolutional layer를 넓히는 방법
  • Convolutional layer의 filter size를 증가시키는 방법

여러 연구에서 작은 filter가 매우 효과적이라는 것이 검증되었기 때문에, 3x3보다 더 큰 filter는 고려하지 않습니다.

 

또한 저자들은 추가적으로 deepening factor $l$와 widening factor $k$라는 2개의 요소를 도입하는데, $l$는 block에서의 convolution의 개수이고 $k$는 convolutional layer에서의 feature의 수에 곱해지는 값입니다.

 

따라서, baseline basic block은 $l = 2, k = 1$에 대응됩니다.

 

Figure 1(a)와 1(c)는 각각 basic block과 basic-wide block을 나타냅니다.

 

 

본 논문에서 사용하는 residual network의 일반적인 구조는 table 1에 나와있습니다.

 

 

처음에 convolutional layer conv1 다음에 3개의 group (각 사이즈는 $N$)의 residual blocks conv2, conv3, conv4 다음에 average pooling, final classification layer가 뒤따르는 구조입니다.

 

conv1의 사이즈는 모든 실험에서 고정이며, 반면에 새롭게 도입된 widening factor $k$는 3개의 그룹 conv2-conv4에서의 residual block의 width를 변경합니다. 

 

본 논문의 저자들은 residual block의 representational power의 효과를 연구하길 원하며, 이를 위해 다음 subsection에서 자세히 설명되어 있는 기본 architecture에 대한 몇 가지 수정을 수행하고 테스트를 진행합니다.

 

 

2.1 Type of convolutions in residual block

 

 

$B(M)$은 residual block structure를 나타내며, $M$은 block 내부에 있는 convolutional layers의 kernel size list를 나타냅니다.

 

예를 들어, $B(3, 1)$은 3x3 convolutional layer와 1x1 convolutional layer를 가지고 있는 residual block을 나타냅니다.

 

이전에 언급했듯이, 본 논문에서는 bottleneck block을 고려하지 않으므로 block마다의 feature planes의 수는 항상 똑같이 유지됩니다.

 

본 논문의 저자들은 basic residual architecture에서 3x3 convolutional layer 각각이 얼마나 중요한지에 대한 질문에 답을 하고 싶었고, 이것이 1x1 layer나 1x1과 3x3 convolutional layer의 조합으로 대체될 수 있는지가 궁금했습니다.

 

따라서, 다음과 같은 조합들을 실험하였습니다.

 

 

2.2 Number of convolutional layers per residual block

 

 

본 논문의 저자들은 또한 block deepening factor $l$이 성능에 어떻게 영향을 미치는지 보기 위해 실험을 진행하였습니다.

 

비교는 같은 수의 파라미터를 가지는 네트워크들 사이에 이루어졌으며, 따라서 이 경우에 저자들은 network complexity를 거의 동일하게 유지하면서 동시에 다른 수의 $l$과 $d$를 가지는 네트워크를 만들었습니다. ($d$는 block의 전체 수)

 

예를 들어서, $l$이 증가할 때, $d$는 감소해야 함을 의미합니다.

 

 

2.3 Width of residual blocks

 

 

저자들은 block의 widening factor $k$에 대해서도 실험을 진행했습니다. 

 

파라미터의 수는 $l$ (deepening factor)와 $d$(ResNet block의 수)에 선형적으로 증가하지만, 파라미터의 수와 computational complexity는 $k$에 대해서는 2차로 증가합니다.

 

하지만 GPU가 large tensor의 병렬 계산에서 훨씬 더 효율적이기 때문에 수천 개의 small kernel을 가지는 것보다 layer를 넓히는 것이 계산적으로 더 효과적이므로, 저자들은 최적의 $d$ to $k$ ratio에 관심을 가졌습니다.

 

 

더 넓은 residual network에 대한 하나의 주장은, 가장 성공적이었던 Inception, VGG를 포함한 residual network 이전의 모든 architecture들은 residual network에 비해서 훨씬 더 넓었다는 것입니다.

 

예를 들어, residual network WRN-22-8과 WRN-16-10(해당 notation에 대한 설명은 다음 paragraph에서 나옵니다.)은 VGG architecture와 parameter의 수, width, depth에서 매우 유사합니다.

 

 

저자들은 더 나아가 $k=1$을 가지는 original residual network를 <<thin>>으로 지칭하고, $k>1$인 네트워크는 <<wide>>로 지칭합니다. 

 

본 논문의 나머지 부분에서는, 다음과 같은 notation을 사용합니다.

 

WRN-$n$-$k$는 convolutional layers의 총개수를 $n$개 가지고 있으며 widening factor $k$를 가지고 있는 residual network를 나타냅니다. (예를 들어, 40개의 layer를 가지고 있고 original보다 2배 더 넓은 경우는 WRN-40-2로 표기)

 

또한, 이 표기에 block type을 덧붙여서 표현할 수 있습니다. 예를 들어, WRN-40-2-B(3, 3)과 같이 표현이 가능합니다.

 

 

2.4 Dropout in residual blocks

 

 

layer를 넓히는 것이 parameter의 수를 증가시키기 때문에, 본 논문의 저자들은 regularization의 방법을 연구하고자 하였습니다.

 

Residual network는 이미 regularization effect를 제공하는 batch normalization을 가지고 있으나, 이는 heavy data augmentation을 요구하고 저자들은 이를 피하고자 하였습니다.

 

따라서 저자들은 fig. 1(d)에 보여지는 것처럼 dropout layer를 residual block 안에 있는 convolution 사이에 추가하였습니다.

 

 

3. Experimental results

 

 

Type of convolutions in a block

 

 

다른 block types $B$를 가지고 학습된 네트워크에 대한 실험 결과를 보고하는 것부터 시작합니다.

 

WRN-40-2를 사용해서 $B(1, 3, 1), B(3, 1), B(1, 3), B(3, 1, 1)$을 block을 사용하였습니다.

 

또한 파라미터의 수를 유지하기 위해서, $B(3, 3), B(3, 1, 3)$의 경우는 더 적은 수의 layer를 가지는 네트워크를 사용하였습니다.

 

Table 2에서는 epoch 당 걸린 시간과 5번 돌렸을 때의 중앙값으로 test accuracy를 포함한 결과를 제공합니다.

 

 

Block $B(3, 3)$이 간소한 차이로 가장 좋다는 사실을 알 수 있었고, $B(3, 1), B(3, 1, 3)$은 $B(3, 3)$과 accuracy에서 유사하지만 더 적은 parameter와 더 적은 layer를 가집니다. 

 

위를 기반으로, 비슷한 수의 parameter를 가진 block들은 거의 유사한 결과를 가져옴이 확인되었습니다.

 

이 사실을 토대로, 저자들은 이후로 3x3 convolution을 가지는 WRN만 사용합니다.

 

 

Number of convolutions per block

 

 

저자들은 다음으로 deepening factor $l$(block 당 convolutional layer의 수)을 다양하게 하는 것과 관련된 실험을 진행했습니다.

 

Table 3에서 결과를 제시하고 있으며, WRN-40-2 with 3x3 convolution을 사용하여 실험을 진행하였다.

 

 

이를 통해 알 수 있듯이, $B(3, 3)$이 최고인 것으로 밝혀졌고 반면에 $B(3, 3, 3), B(3, 3, 3, 3)$은 더 안 좋은 성능을 냈습니다.

 

결론적으로, $B(3, 3)$이 block 당 convolution의 수 관점에서 최적이라는 것입니다.

 

따라서 남은 실험에서 저자들은 오직 $B(3, 3)$ block을 가진 wide residual network만 사용하였습니다.

 

 

Width of residual blocks

 

 

저자들이 widening parameter $k$를 증가시키려고 시도할 때, layer의 총숫자를 감소시켜야만 했습니다.

 

최적의 비율을 찾기 위해서, 저자들은 $k$를 2부터 12까지, depth는 16부터 40까지 실험하였으며, 결과는 Table 4에서 확인할 수 있습니다.

 

 

 

Table 4에서 볼 수 있는 것처럼, 40, 22, 16 layer를 가지는 모든 네트워크는 width가 1부터 12배까지 증가함에 따라서 일관적인 증가를 보여주고 있습니다. (논문에는 이렇게 쓰여있었는데, 보니까 22 layer는 CIFAR-10에서는 감소했고, CIFAR-100에서는 증가했습니다.)

 

반면에, 동일한 고정된 widening factor $k = 8$ or $k = 10$을 유지할 때, 그리고 depth를 16에서 28까지 다양하게 했을 때 일관적으로 성능 향상이 있었습니다. 

 

하지만, depth가 40까지 증가했을 때 accuracy는 오히려 감소했습니다. (즉, WRN-40-8은 WRN-22-8보다 더 낮은 accuracy를 가집니다.)

 

 

본 논문의 저자들은 얇은 residual network와 넓은 residual network를 비교한 추가적인 결과를 Table 5에서 제공합니다.

 

 

 

wide WRN-40-4와 thin ResNet-1001을 비교했을 때, wide WRN이 CIFAR-10과 CIFAR-100에서 모두 더 나은 accuracy를 보여줌을 확인할 수 있습니다. 

 

또한, wide WRN-28-10은 thin ResNet-1001보다 36배 더 적은 layer를 가지고 CIFAR-10에서 0.92%, CIFAR-100에서 3.46% 더 좋은 성능을 냈습니다.

 

 

Figure 2에서는 CIFAR-10과 CIFAR-100에서의 training curve를 나타내고 있습니다.

 

 

Depth가 regularization effect를 주고, width는 network가 overfitting 되도록 야기한다는 이전의 주장에도 불구하고, 본 논문의 저자들은 ResNet-1001보다 몇 배 더 많은 수의 parameter를 가진 network를 성공적으로 학습했습니다.

 

일반적으로, 저자들은 CIFAR mean/std preprocessing이 wider and deeper network를 더 좋은 정확도를 가지고 학습하도록 만들어준다는 사실을 발견하였다고 합니다.

 

지금까지의 실험 내용을 요약하자면,

 

  • Widening은 다른 depth를 가지는 residual network 사이에서 일관적으로 성능을 향상한다.
  • Depth와 width를 모두 증가시키는 것은 parameter의 수가 너무 높아지거나 혹은 더 강력한 regularization이 요구될 때까지는 도움이 된다.
  • 매우 깊은 깊이를 가지는 residual network에서 어떠한 regularization effect가 보이지 않는데, 이는 같은 수의 parameter를 가지는 thin network와 비교했을 때 wide network가 동일하거나 혹은 더 나은 representation을 학습할 수 있기 때문이다. 또한, wide network는 thin network보다 2배 또는 그 이상의 parameter를 성공적으로 학습할 수 있다. 

 

Dropout in residual blocks

 

 

본 논문의 저자들은 모든 데이터셋에 대해서 residual block 안에 convolution 사이에 dropout을 삽입한 network를 학습했다. 

 

저자들은 dropout probability value를 결정하기 위해 cross-validation을 사용하였으며, CIFAR에서는 0.3이었다. 

 

 

Dropout은 WRN-28-10을 가지고 CIFAR-10과 CIFAR-100에 대해서 test error를 감소시켰으며, 다른 ResNet에서도 동일한 성능 향상을 가져왔다.

 

 

저자들은 thin network와 wide network 모두에게서 dropout을 사용하였을 때 상당한 성능 향상을 관측할 수 있었다.

 

전체적으로, batch normalization과 결합한다는 주장에도 불구하고, dropout은 그 자체로 thin network와 wide network에서의 효과적인 regularization 기술이다. 

 

 

Computational efficiency

 

 

Width를 증가시키는 것은 효과적으로 computation을 균형 맞추는 데 더욱 최적의 방식으로 도움을 주며, 따라서 thin network보다 wide netowrk가 몇 배는 더 효율적이다.

 

저자들은 cudnn v5와 Titan X를 사용해 여러 가지 네트워크에 대해 minibatch size 32를 가지고 forward + backward update time을 측정했으며, 결과는 figure 4와 같다.

 

 

CIFAR에서 최고의 성능을 낸 WRN-28-10가 thin ResNet-1001보다 1.6배 더 빠르다.

 

게다가, wide WRN-40-4는 ResNet-1001와 거의 유사한 accuracy를 가지지만, 8배 더 빠르다.

 

 

Implementation details

 

 

본 논문의 모든 실험에서 Nesterov momentum을 활용하는 SGD와 cross-entropy loss를 사용하였다.

 

Initial learning rate는 0.1이고, weight decay는 0.0005고, momentum은 0.9며 minibatch size는 128이다.

 

CIFAR에서 learning rate는 60, 120, 160 epochs에서 0.2씩 떨어지고, 총 200 epochs동안 학습했다.

 

 

 

4. Conclusions

 

 

본 논문에서는 residual network의 width에 대한 연구와 residual architecture에서 dropout의 사용에 대한 연구를 제시하였습니다.

 

이 연구를 기반으로, 저자들은 여러가지 benchmark dataset에 대해서 SOTA results를 가져오는 wide residual network architecture를 제안합니다.

 

저자들은 16 layer를 가지는 wide network가 1000-layer deep network에 비해서 상당히 더 좋은 성능을 낸다는 것을 검증하였으며, 따라서 residual network의 주요한 힘은 극단적인 depth에 있는 것이 아니라 residual blocks에 있음을 검증하였습니다.

 

또한, wide residual network는 학습하는데 몇 배 더 빠르게 학습됩니다.

 

 

 

여기까지 Wide Residual network 논문에 대해서 살펴보았습니다.

 

기존 ResNet이 depth에 초점을 두었다면, 본 논문은 width에 초점을 두고 연구를 진행했으며 dropout을 추가했고 BN와 ReLU, conv의 architecture 순서에 대한 연구를 통해 순서를 바꾸었다는 점이 가장 핵심이 되는 연구라고 보입니다.

 

다음 글에서는 해당 모델에 대한 code review를 진행해보도록 하겠습니다.

 

감사합니다.

 

 

 

 

 

 

 

안녕하세요.

 

이번 글에서는 ResNet의 code를 리뷰해보겠습니다.

 

ResNet 논문에 대한 내용은 이전 글에서 확인하실 수 있습니다.

 

cumulu-s.tistory.com/33

 

6. Deep Residual Learning for Image Recognition(ResNet) - paper review

안녕하세요. 오늘은 Deep Residual Learning for Image Recognition(ResNet) 논문에 대해서 정리해보겠습니다. 논문 주소: arxiv.org/abs/1512.03385 Deep Residual Learning for Image Recognition Deeper neural..

cumulu-s.tistory.com

 

code review 시작해보겠습니다.

 

 

import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import torch.backends.cudnn as cudnn
from tqdm.auto import tqdm
import torchvision
import torchvision.transforms as transforms
import datetime
import os
from torch.utils.tensorboard import SummaryWriter
import shutil

먼저, 필요한 package들을 가져옵니다.

 

device = 'cuda' if torch.cuda.is_available() else 'cpu'

# Data
print('==> Preparing data..')
transform_train = transforms.Compose([
    transforms.RandomCrop(32, padding=4),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
])

transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)),
])

trainset = torchvision.datasets.CIFAR10(
    root='/content/drive/MyDrive/CIFAR10', train=True, download=True, transform=transform_train)
trainloader = torch.utils.data.DataLoader(
    trainset, batch_size=128, shuffle=True, num_workers=2)

testset = torchvision.datasets.CIFAR10(
    root='/content/drive/MyDrive/CIFAR10', train=False, download=True, transform=transform_test)
testloader = torch.utils.data.DataLoader(
    testset, batch_size=100, shuffle=False, num_workers=2)

current_time = datetime.datetime.now() + datetime.timedelta(hours= 9)
current_time = current_time.strftime('%Y-%m-%d-%H:%M')

saved_loc = os.path.join('/content/drive/MyDrive/ResNet_Result', current_time)
if os.path.exists(saved_loc):
    shutil.rmtree(saved_loc)
os.mkdir(saved_loc)

print("결과 저장 위치: ", saved_loc)

writer = SummaryWriter(saved_loc)

사용할 데이터셋을 가져오는 코드들입니다.

 

 

 

class BasicBlock(nn.Module):
    expansion = 1

    def __init__(self, in_planes, planes, stride=1):
        super(BasicBlock, self).__init__()
        self.conv1 = nn.Conv2d(
            in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,
                               stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)

        self.shortcut = nn.Sequential()
        if stride != 1 or in_planes != self.expansion*planes:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_planes, self.expansion*planes,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(self.expansion*planes)
            )

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += self.shortcut(x)
        out = F.relu(out)
        return out

첫 번째로 볼 부분은, ResNet에서 사용하는 BasicBlock입니다.

 

논문에서 ResNet18과 ResNet34에 사용되는 Building Block이죠.

 

in_planes와 planes, stride라는 변수를 받아서 생성되게 되며

 

3x3 conv - BN - 3x3 conv - BN - shortcut으로 구성되는 코드라고 보시면 됩니다.

 

논문에서는 다음 그림으로 표현되었죠.

 

 

self.shortcut = nn.Sequential()은 연결만 해주는 코드로, input을 그대로 연결해주는 역할을 합니다.

 

if문은 stride가 1이 아니거나 in_planes != self.expansion*planes인 경우(즉, input으로 들어온 차원과 output에 해당하는 차원이 다른 경우)에 해당하게 되는데, 이 때는 1x1 conv를 진행하도록 해서 차원이 일치하도록 만들어줍니다. 

 

 

만약 input이 32x32x64이고 stride가 2로 설정되어 있는 경우의 shape를 나타낸 그림입니다.

 

k는 kernel size를 의미하고, s는 stride, p는 padding을 의미합니다.

 

input인 32x32x64이었으나, layer가 거치면서 16x16x128가 되었으므로 input을 그대로 더해줄 수 없게 됩니다.

 

따라서, 1x1 conv를 이용해서 channel 수를 맞춰주면서 동시에 stride = 2를 적용하여 가로, 세로 길이도 /2 해서 16x16x128를 맞춰주게 되면 shortcut을 더해줄 수 있게 되는 것입니다.

 

 

input의 shape와 layer를 통과하고 나서의 shape가 동일한 경우는, 다음과 같이 살펴볼 수 있습니다.

 

 

앞에서도 언급했지만, nn.Sequential()은 말 그대로 input 값을 그대로 가지고 있다고 생각하시면 됩니다.

 

BasicBlock에 대한 설명은 이쯤이면 충분한 것 같아 다음 block으로 넘어가겠습니다.

 

class Bottleneck(nn.Module):
    expansion = 4

    def __init__(self, in_planes, planes, stride=1):
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(planes)
        self.conv2 = nn.Conv2d(planes, planes, kernel_size=3,
                               stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(planes)
        self.conv3 = nn.Conv2d(planes, self.expansion *
                               planes, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(self.expansion*planes)

        self.shortcut = nn.Sequential()
        if stride != 1 or in_planes != self.expansion*planes:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_planes, self.expansion*planes,
                          kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(self.expansion*planes)
            )

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = F.relu(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        out += self.shortcut(x)
        out = F.relu(out)
        return out

 

다음에 설명할 코드는 Bottleneck 코드입니다.

 

ResNet의 layer가 50층 이상 넘어갈 경우, 연산량을 줄여주기 위해 기존의 3x3 conv를 두 개 쌓아서 만든 layer를 사용하는 것이 아닌, 1x1 conv - 3x3 conv - 1x1 conv를 이용하는 방식을 사용합니다.

 

이번에도 if문에 해당하는 경우 어떻게 흘러가는지를 설명드리도록 하겠습니다.

 

input이 32x32x256인 경우의 상황을 보여주고 있는 그림입니다.

 

먼저, 1x1 conv을 이용해서 차원을 128차원으로 줄여줍니다. 

 

그러고 나서, 3x3 conv 연산을 진행하고, 1x1 conv 연산을 통해서 다시 512차원으로 늘려주게 됩니다.

 

input이 32x32x256이었으므로, shortcut은 input을 그대로 전달해주면 안 됩니다.

 

따라서 Basicblock과 마찬가지로 1x1 conv을 통해서 차원을 맞춰주면서 동시에 stride = 2를 적용해 가로, 세로 사이즈를 /2 해줍니다.

 

다음으로는 stride가 1인 경우에 대해서 보여드리겠습니다.

 

 

stride가 2로 되어서 가로, 세로가 /2이 되는 경우가 아니라면, channel의 수만 중간에 한번 변경하게 됩니다.

 

이는 3x3 conv를 적용할 때 더 적은 차원의 데이터만 적용하도록 만들어서 연산량을 줄이기 위함입니다.

 

이 경우에는 input과 layer를 통과한 output의 shape가 같으므로, nn.Sequential()을 이용해서 shortcut을 진행해주면 됩니다.

 

 

class ResNet(nn.Module):
    def __init__(self, block, num_blocks, num_classes=10):
        super(ResNet, self).__init__()
        self.in_planes = 64

        self.conv1 = nn.Conv2d(3, 64, kernel_size=3,
                               stride=1, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1)
        self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)
        self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2)
        self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)
        self.linear = nn.Linear(512*block.expansion, num_classes)

    def _make_layer(self, block, planes, num_blocks, stride):
        strides = [stride] + [1]*(num_blocks-1)
        layers = []
        for stride in strides:
            layers.append(block(self.in_planes, planes, stride))
            self.in_planes = planes * block.expansion
        return nn.Sequential(*layers)

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        out = F.avg_pool2d(out, 4)
        out = out.view(out.size(0), -1)
        out = self.linear(out)
        return out

 

다음으로는 ResNet class입니다. 

 

논문 리뷰할 때 ResNet 전체 architecture 표에서는 맨 처음 conv가 7x7로 되어 있었는데, 논문의 CIFAR-10 실험한 부분을 보면 CIFAR-10의 데이터가 32x32x3 임을 감안해서 architecture가 살짝 변경되었음을 확인할 수 있습니다.

 

따라서, 해당 코드는 CIFAR-10에 대해 실험을 진행했기 때문에 구조가 살짝 다릅니다. (핵심적인 ResNet 구조는 동일).

 

ResNet을 만들 때는 block과 num_blocks를 입력으로 받습니다. (def __init__의 parameter를 보시면 됩니다.)

 

맨 처음에 3x3 conv - BN을 적용해주고, 그 이후에는 _make_layer 함수를 이용해서 block이 포함된 layer들을 만들어줍니다.

 

여기서 block이란, 앞에서 제가 설명드린 BasicBlock이나 Bottleneck block을 의미합니다. (앞에서 input으로 block을 받는다고 얘기했었죠?)

 

함수가 생각보다는 복잡하기 때문에, 간단하게 하나 예시를 들어서 하나하나 따라가 보겠습니다.

 

 

가장 간단한 형태였던 ResNet18를 예로 들어보도록 하겠습니다.

 

ResNet18은 Basicblock을 사용하며, 각 layer 마다 block을 2개씩 사용합니다.

 

따라서, ResNet18을 만든다고 한다면 net = ResNet(BasicBlock, [2, 2, 2, 2])로 만들 수 있습니다.

 

먼저 input은 32x32x3이 될 것이고요, 첫 번째로 self.conv1 = nn.Conv2d(3, 64, k = 3, s = 1, p = 1)를 통과해줍니다. output은 32x32x64이 됩니다. 여기에 self.bn1 = nn.BatchNorm2d(64)를 통과해줍니다. BN은 shape에 영향을 안 주니 shape는 그대로입니다.

 

다음으로는 self.layer1 = self._make_layer(BasicBlock, 64, 2, stride = 1)을 통과합니다.

 

_make_layer 함수로 들어가서 보면,

 

strides = [1, 1]이 될 것이고, for stride in strides:의 반복문을 적용하게 되면 

 

layers라는 빈 list에 BasicBlock(64, 64, 1)와 BasicBlock(64, 64, 1)을 넣어주고 self.in_planes는 그대로 64입니다. (block.expansion = 1이고, planes = 64이기 때문이죠.)

 

_make_layer는 nn.Sequential(*layers)를 return 하므로, nn.Sequential(BasicBlock(64, 64, 1), BasicBlock(64, 64, 1)) 을 return 합니다. 이를 self.layer1으로 저장하는 것이죠.

 

BasicBlock은 in_planes, planes, stride라고 하는 3개의 parameter를 입력받게 되고, 이것을 각각 (64, 64, 1), (64, 64, 1)로 받았습니다.

 

먼저 self.conv1 = nn.Conv2d(64, 64, k=3, s= 1, p= 1) => self.bn1(nn.BatchNorm2d(64)) => self.conv2 = nn.Conv2d(64, 64, k=3, s=1, p=1) => self.bn2(nn.BatchNorm2d(64)) => self.shortcut으로 연결 => F.relu => out으로 연결되는 구조입니다. 

 

stride가 모두 1이므로, 가로와 세로의 사이즈는 변동되지 않으며, channel 수는 여전히 64가 됩니다.

 

BasicBlock이 두 개였으므로, 위의 과정을 동일하게 두 번 거쳐서 나오면 ResNet18 기준에서 self.layer1을 통과한 것이라고 보시면 되겠습니다.

 

 

다음으로는 self.layer2인데요, 이는 self._make_layer(BasicBlock, 128, 2, stride = 2)를 적용합니다.

 

_make_layer 함수로 들어가게 되면, strides가 이번에는 [2, 1]가 됩니다. 아까는 [1, 1]이었죠.

 

그리고 똑같이 for stride in strides:의 반복문을 이용해서 layers.append를 해주는데, BasicBlock(64, 128, 2)와 BasicBlock(128, 128, 1)을 append 해줍니다. 그리고 이 때는 self.in_planes = planes * block.expansion인데 planes가 128이었으므로, self.in_planes가 64에서 128로 값이 변경됩니다.

 

앞과 동일하게 def _make_layer는 nn.Sequential(*layers)를 return 해주므로, ResNet 기준에서 self.layer2는 nn.Sequential(BasicBlock(64, 128, 2), BasicBlock(128, 128, 1))이 될 것입니다.

 

 

다시 BasicBlock 쪽으로 가봐야겠죠?

 

이번에는 in_planes가 64, planes가 128, stride = 2입니다.

 

이번에는 BasicBlock의 input이 아까 ResNet 기준 self.layer1에서의 output인 32x32x64가 됩니다.

 

self.conv1 = nn.Conv2d(64, 128, k=3, s = 2, p= 1) 이므로, 이를 통과해주면 16x16x128이 되고, self.bn1 = nn.BatchNorm2d(128)을 통과해줍니다.

 

다음으로는 self.conv2 = nn.Conv2d(128, 128, k=3, s= 1, p= 1)를 통과해주고, 이 때는 shape의 변동이 없습니다. self.bn2 = nn.BatchNorm2d(128)도 마찬가지죠.

 

그리고 아까와 달라지는 점은, stride가 1이 아니므로, BasicBlock의 if문에 들어갑니다. 이 때는 self.shortcut이 달라지게 되는데요.

 

self.shortcut = nn.Sequential( nn.Conv2d(64, 128, k=1, s = 21), nn.BatchNorm2d(128))가 됩니다.

 

따라서 input이었던 32x32x64를 16x16x128로 바꾸어서 shortcut이 이루어질 수 있도록 바꿔주게 됩니다.

 

따라서 ResNet 기준 self.layer2의 output은 16x16x128가 될 것입니다.

 

ResNet 기준 self.layer3, self.layer4는 동일한 방법으로 리딩 할 수 있으니 생략하겠습니다.

 

ResNet으로 다시 돌아와서, def forward 부분을 보게 되면 self.layer4가 끝난 후, F.avg_pool2d를 적용하는 것을 확인할 수 있습니다.

 

self.layer2의 output은 16x16x128이고, self.layer3의 output은 8x8x256, self.layer4의 output은 4x4x512입니다.

 

따라서, 4x4x512에 average pooling을 적용해서 이를 1x1x512로 만들어줍니다.

 

그리고. view를 이용해서 이를 Batch sizex512로 바꿔주게 되고, 이를 self.linear와 연결합니다. self.linear는 nn.Linear(512, 10)이므로, 이를 통과하면 Batch size x 10가 되겠죠? 이를 최종 예측값으로 사용해줍니다.

 

여기까지가 ResNet class에 대한 설명이 되겠습니다.

 

 

net = ResNet(BasicBlock, [2, 2, 2, 2]) # ResNet18

net = net.to(device)
cudnn.benchmark = True

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.1,
                      momentum=0.9, weight_decay=5e-4)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=200)

best_acc = 0

net이라는 변수로 ResNet을 생성해주고,. to를 이용해서 GPU로 모델을 이동시켜줍니다.

 

Loss는 Cross Entropy loss를 사용해주고, optimizer는 SGD를 사용합니다. 

 

scheduler는 CosineAnnealingLR이라고 하는 것을 사용했는데, 제가 참고한 자료에 있어서 한번 써봤습니다.

 

해당 scheduler를 사용하면, learning rate가 다음과 같은 형태로 변화합니다.

 

 

 

# Training
def train(epoch):
    print('\nEpoch: %d' % epoch)
    net.train()
    train_loss = 0
    correct = 0
    total = 0
    batch_count = 0
    for batch_idx, (inputs, targets) in enumerate(trainloader):
        inputs, targets = inputs.to(device), targets.to(device)
        optimizer.zero_grad()
        outputs = net(inputs)
        loss = criterion(outputs, targets)
        loss.backward()
        optimizer.step()

        train_loss += loss.item()
        _, predicted = outputs.max(1)
        total += targets.size(0)
        correct += predicted.eq(targets).sum().item()
        batch_count += 1

    print("Train Loss : {:.3f} | Train Acc: {:.3f}".format(train_loss / batch_count, 100.*correct/total))
    final_loss = train_loss / batch_count
    final_acc = 100.*correct / total
    return final_loss, final_acc

 

다음은 Training 코드입니다.

 

먼저 신경망을 train 모드로 만들어주고, 필요한 변수들을 지정해줍니다.

 

batch size 단위로 받은 이미지를 net에 투입해서 예측 결과인 outputs을 만들어냅니다.

 

그리고 정답 값인 targets와 loss를 계산해주고, 이를 backpropagation 해줍니다. 

 

계산된 loss는 train_loss라는 변수에 계속해서 누적해서 더해줍니다.

 

outputs.max(1)을 하면 두 개의 값이 나오게 되는데요, 이는 outputs에서 max값을 나타내는 값과 해당 값의 index가 나오게 됩니다. 

 

즉, batch size가 128이라면, outputs은 (128, 10)이며, outputs.max(1)을 통해 나오는 값은 차원이 (128), (128)이 되는 것입니다.

 

우리는 예측된 index만 필요하므로, 값 부분은 _로 처리해주고 predicted라는 변수로 index를 저장해줍니다.

 

그리고 이 predicted와 targets 값이 같은지를 predicted.eq(targets)를 이용해서 계산해주고, 이는 같은지 아닌지를 1과 0으로 저장하게 되므로 sum()을 해서 배치 사이즈인 128개 중에서 몇 개가 정답을 맞혔는지 계산합니다.

 

. item()은 torch.tensor에서 값만 빼주는 코드입니다. 따라서, 이를 통해 128개 중에서 몇 개가 정답을 맞혔는지를 숫자 값으로 저장하게 되죠.

 

 

def test(epoch):
    global best_acc
    net.eval()
    test_loss = 0
    correct = 0
    total = 0
    batch_count = 0
    with torch.no_grad():
        for batch_idx, (inputs, targets) in enumerate(testloader):
            inputs, targets = inputs.to(device), targets.to(device)
            outputs = net(inputs)
            loss = criterion(outputs, targets)

            test_loss += loss.item()
            _, predicted = outputs.max(1)
            total += targets.size(0)
            correct += predicted.eq(targets).sum().item()
            batch_count += 1


    print("Test Loss : {:.3f} | Test Acc: {:.3f}".format(test_loss / batch_count, 100.*correct/total))

    # Save checkpoint.
    acc = 100.*correct/total
    if acc > best_acc:
        print('Saving..')
        state = {
            'net': net.state_dict(),
            'acc': acc,
            'epoch': epoch,
        }
        if not os.path.isdir(os.path.join(saved_loc, 'checkpoint')):
            os.mkdir(os.path.join(saved_loc, 'checkpoint'))
        torch.save(state, os.path.join(saved_loc, 'checkpoint/ckpt.pth'))
        best_acc = acc
    
    final_loss = test_loss / batch_count
    return final_loss, acc

 

다음은 test 코드입니다.

 

train 하고 대부분은 큰 차이가 없으므로, 겹치는 부분은 설명을 생략하겠습니다.

 

test에는 checkpoint를 저장하는 부분이 있는데, 먼저 accuracy를 계산해줍니다.

 

그리고 best_acc보다 acc가 크면, best_acc를 acc로 바꿔줍니다. 

 

이때, 신경망의 가중치와, accuracy, epoch 값을 state로 저장하여 이를 torch.save로 저장해줍니다.

 

이를 통해, test 기준에서 가장 좋은 성능을 내는 모델을 저장하도록 합니다.

 

 

for epoch in tqdm(range(200)):
    train_loss, train_acc = train(epoch)
    test_loss, test_acc = test(epoch)

    writer.add_scalars('Loss', {"Train Loss" : train_loss, "Test Loss" : test_loss}, epoch)
    writer.add_scalars('Accuracy', {"Train acc" : train_acc, "test acc" : test_acc}, epoch)
    
    scheduler.step()

writer.close()

 

200 에폭 동안 train과 test를 진행합니다.

 

scheduler.step()은 각 epoch마다 적용되도록 해줍니다.

 

 

 

 

해당 코드를 돌려서 얻을 수 있는 결과는 다음과 같습니다.

 

 

 

위는 accuracy, 아래는 loss graph입니다.

 

epoch이 진행됨에 따라서, training accuracy와 test accuracy 모두 상승하는 것을 확인할 수 있습니다.

 

loss 또한 두 loss가 모두 잘 감소하고 있는 것을 확인할 수 있습니다.

 

 

 

이번 글에서는 ResNet을 코드로 구현한 것에 대해서 살펴보고, 결과까지 살펴보았습니다.

 

오늘 글에서 사용된 코드는 제 github에서 전체 내용을 확인하실 수 있습니다.

 

github.com/PeterKim1/paper_code_review

 

PeterKim1/paper_code_review

paper review with codes. Contribute to PeterKim1/paper_code_review development by creating an account on GitHub.

github.com

 

다음 글에서 뵙겠습니다.

 

감사합니다.

 

 

 

 

 

 

 

 

안녕하세요. 

 

 

오늘은 Deep Residual Learning for Image Recognition(ResNet) 논문에 대해서 정리해보겠습니다.

 

 

논문 주소: arxiv.org/abs/1512.03385

 

Deep Residual Learning for Image Recognition

Deeper neural networks are more difficult to train. We present a residual learning framework to ease the training of networks that are substantially deeper than those used previously. We explicitly reformulate the layers as learning residual functions with

arxiv.org

 

이전에는 논문을 한 줄 한 줄 세세하게 정리했었는데, 작업하는데 시간이 매우 오래 걸리고 어떤 것이 핵심인지 저 조차 파악하기가 힘들어져서 분량을 줄이고, 핵심적인 부분을 정리하는 방향으로 바꾸려고 합니다.

 

 

시작하겠습니다.

 

 

Abstract

 

 

깊은 신경망은 학습시키기가 더욱 어렵습니다. 

 

본 논문에서는, 이전에 사용했던 것보다 상당히 더 깊은 네트워크의 학습을 쉽게 만들기 위해서 residual learning framework를 제시합니다.

 

Layer들이 unreferenced function을 학습하는 것 대신에, layer의 input에 대해서 residual function을 학습하는 것으로 다르게 표현합니다.

 

본 논문에서는, 이러한 residual network가 최적화하기 쉽고, 증가된 depth로부터 더 좋은 accuracy를 얻을 수 있다는 실증적인 증거를 보였습니다.

 

ImageNet dataset에 대해서는 residual nets을 152 layers까지 평가하였고, 이는 VGGnet보다 8배 더 깊지만 낮은 complexity를 가집니다.

 

Residual net의 앙상블을 이용해서 ImageNet test set에서 3.57% error를 달성하였으며, 이는 ILSVRC 2015 classification task에서 1등을 수상하였습니다. 

 

 

 

1. Introduction

 

 

Deep convolutional neural network는 image classification에서 일련의 돌파구를 이끌었습니다.

 

Deep network는 자연스럽게 low/mid/high-level feature와 classifier를 end-to-end multi-layer 형식으로 통합하며, feature의 level은 쌓인 layer의 수 (깊이)에 의해 풍부해질 수 있습니다.

 

최근, network 깊이가 중요하다는 사실이 드러났으며, ImageNet dataset에 대해서 좋은 결과를 낸 경우는 모두 매우 깊은 모델을 사용하였습니다. 

 

 

깊이와 관련해서, 다음과 같은 질문이 존재합니다.

 

"더 많은 레이어를 쌓는 것만큼 더 나은 네트워크를 학습하는 것이 쉬운가?"

이와 관련된 문제는 vanishing/exploding gradient의 문제이며, 이는 초기부터 모델의 수렴을 방해합니다.

 

하지만, 이 문제는 주로 normalized initialization이나, 중간 normalization layer에 의해서 해결되어 왔으며 이를 통해 수십 개의 layer로 구성된 network가 backpropagation을 가지고 SGD를 이용해 수렴할 수 있도록 할 수 있었습니다.

 

 

깊은 network가 수렴하기 시작할 수 있을 때, degradation 문제가 제기되었습니다.

 

Degradation이란, network의 깊이가 증가할수록 accuracy가 saturate 되며, 빠르게 저하되는 것을 의미합니다. 

 

이러한 degradation은 overfitting에 의해서 일어나는 것이 아니며, 더 많은 레이어를 추가했을 때 더 높은 training error를 야기합니다.

 

Fig.1는 degradation의 전형적인 예시를 보여주는 그래프입니다.

 

 

본 논문에서는 deep residual learning framework를 도입하여 degradation problem을 해결합니다.

 

각 layer들이 직접적으로 바람직한 underlying mapping에 fitting 되도록 바라는 것 대신에, residual mapping에 fitting 되도록 하는 것입니다.

 

형식적으로 표현해보자면, 바람직한 underlying mapping을 $\mathcal{H}(x)$라고 표현하고, stacked nonlinear layers는 $\mathcal{F}(x) := \mathcal{H}(x) - x$의 mapping을 fitting 하도록 합니다. 

 

원래의 mapping을 표현하자면 $\mathcal{F}(x) + x$로 나타낼 수 있습니다.

 

본 논문에서는 원래 unreferenced mapping을 최적화하는 것보다 residual mapping을 최적화하는 것이 쉬울 것이라는 가설을 세웠습니다.

 

극단적으로, 만약 identity mapping이 최적이라면, layer들이 identity mapping을 fitting 하는 것보다 residual을 0으로 만드는 것이 쉬울 것입니다.

 

 

$\mathcal{F}(x) + x$의 formulation은 "shortcut connections"를 가진 feedforward neural network에 의해서 나타내질 수 있습니다. 

 

 

Shortcut connections은 1개 혹은 더 많은 수의 layer를 skipping 하는 것입니다.

 

본 논문의 경우에서는, shortcut connection은 단순히 identity mapping을 수행하게 됩니다.

 

따라서, 별도의 parameter나 computational complexity를 추가하지 않습니다.

 

전체 네트워크는 여전히 SGD를 가지고 backpropagation에 의해서 end-to-end로 학습될 수 있습니다.

 

 

본 논문에서는 degradation problem을 제시하고 residual network를 평가하기 위해서 ImageNet에서 광범위한 실험을 제시합니다.

 

1) 극단적으로 깊은 residual net이 최적화하기 쉬우며, 반면에 counterpart인 "plain" net (단순히 레이어를 쌓은 경우)는 깊이가 증가했을 때 더 높은 training error를 나타냄을 보입니다.

 

2) Deep residual network는 깊이를 증가할수록 더 높은 accuracy를 얻을 수 있음을 보입니다.

 

 

유사한 현상이 CIFAR-10 dataset에서도 나타나며, 최적화의 어려움과 residual net의 효과가 단순히 특정한 데이터셋에만 적용되는 것이 아님을 추가적으로 보입니다.

 

 

2. Related Work

 

생략

 

 

3. Deep Residual Learning

 

3.1 Residual Learning

 

 

$\mathcal{H}(x)$를 몇 개의 stacked layers (필수적으로 전체 네트워크여야 하는 것은 아닙니다.)에 의해 fitting 되어야 하는 underlying mapping이라고 하고, $x$는 이러한 layer의 input을 나타냅니다.

 

만약 multiple nonlinear layers가 점근적으로 복잡한 함수들을 근사할 수 있다고 가설을 세운다면, 이는 점근적으로 $\mathcal{H}(x) -x$ (이때, input과 output은 동일한 차원을 가짐을 가정합니다.)와 같은 residual function도 근사할 수 있다는 것과 동일합니다.

 

따라서, stacked layers가 $\mathcal{H}(x)$를 근사하기를 기대하기 보다도, $\mathcal{F}(x) := \mathcal{H}(x) -x$를 근사하도록 합니다.

 

즉, original function은 $\mathcal{F}(x) + x$가 됩니다. 

 

 

3.2 Identity Mapping by Shortcuts

 

 

본 논문에서 제시하는 모델에서는, 모든 stacked layer에 residual learning을 채택합니다. 

 

Building block은 Fig.2에서 제시되었으며, 공식적으로 다음과 같이 정의할 수 있습니다.

 

 

 

$x$와 $y$는 고려된 layer의 input과 output vector입니다. 함수 $\mathcal{F}(x, {W_i})$는 학습되어야 할 residual mapping을 나타냅니다.

 

예를 들어, two layer를 가지는 Fig. 2에서 $\mathcal{F} = W_2\sigma(W_1x)$이고, $\sigma$는 ReLU를 나타내며 notation의 단순화를 위해서 bias는 생략하였습니다.

 

Operation $\mathcal{F} + x$는 shortcut connection과 element-wise addition에 의해서 수행되며, addition 후에 두 번째 nonlinear function을 적용합니다.

 

 

Eqn. (1)에 있는 shortcut connections은 추가적인 parameter나 computation complexity를 도입하지 않습니다.

 

이는 실제로 구현할 때 매력적일 뿐 아니라, plain과 residual network 사이의 비교에 있어서도 중요합니다.

 

 

$x$와 $\mathcal{F}$의 차원은 Eqn. (1)에서 동일해야 하며, 그렇지 않은 경우에는 차원을 맞추기 위해 shortcut connection에 의한 linear project $W_s$를 수행할 수 있습니다.

 

 

 

물론, Eqn.(1)에서도 square matrix $W_s$를 사용할 수 있지만, 실험을 통해서 identity mapping이 degradation problem을 해결하는데 충분하고 경제적임을 확인하여서 $W_s$는 오직 차원을 맞출 때만 사용됩니다.

 

 

Residual function $\mathcal{F}$의 형태는 유연합니다. 

 

본 논문에서 실험을 할 때는 two or three layer를 포함하는 function $\mathcal{F}$를 사용했지만, 더 많은 layer도 가능합니다. 

 

하지만, 만약 $\mathcal{F}$가 single layer라면, Eqn.(1)는 linear layer와 유사해집니다. ($y = W_1x + x$). 이런 경우, 어떤 이점도 가지지 못하게 됩니다.

 

 

notation을 단순하게 하기 위해서 fully-connected로 나타냈지만, 이는 convolutional layer에도 적용이 가능합니다.

 

함수 $\mathcal{F}(x, {W_i})$는 multiple convolutional layer를 나타낼 수 있으며, element-wise addition은 channel by channel로 두 feature map에 수행됩니다.

 

 

3.3 Network Architectures

 

 

저자들은 다양한 plain / residual net을 테스트했고, 일관적인 현상을 발견했다고 합니다. 논의를 위한 예시를 제공하고자, ImageNet에 대해 두 모델을 나타냅니다.

 

 

 

Plain Network. plain baselines (Fig. 3, middle)은 주로 VGGnet의 철학에 의해 영감을 받아서 만들어졌습니다. (Fig. 3, left)

 

convolutional layer는 대부분 3x3 filter를 가지고 있으며 두 가지 간단한 규칙을 따릅니다.

 

(i) 같은 output feature map size에 대해서, layer는 같은 수의 filter를 가진다.

 

(ii) 만약 feature map size가 절반이 되면, filter의 사이즈는 두 배로 해서 layer 당 time complexity를 보존합니다.

 

해당 network에서는 stride를 2로 설정한 convolutional layer에 의해서 직접적으로 downsampling이 수행됩니다.

 

네트워크는 global average pooling layer로 끝나고, 1000-way fully-connected layer with softmax로 끝납니다.

 

전체 가중치가 있는 layer는 34개이며, 해당 모델은 VGGnet에 비해서 더 적은 filter와 더 낮은 복잡도를 가집니다.

 

 

Residual Network. 위의 Plain network에 기반해서, 이 network를 residual version으로 바꿔주는 shortcut connection을 추가합니다.

 

Identity shortcuts은 input과 output이 같은 차원을 가질 때 직접적으로 사용될 수 있습니다. (Fig. 3에서 실선)

 

차원이 증가할 때, (Fig. 3에 있는 점선) 두 가지 선택을 고려해볼 수 있습니다.

 

(A) shortcut은 여전히 identity mapping을 수행하나, 차원을 증가시키기 위해 zero padding을 해준다.

 

이 선택은 추가적인 parameter를 도입하지 않습니다.

 

(B) Eqn.(2)에 나타난 projection shortcut을 사용하여 차원을 맞춘다. (1x1 conv을 이용하게 됩니다.)

 

 

3.4 Implementation 

 

해당 파트는 실험 관련된 부분이므로, 필요하다면 논문을 직접 읽어보시면 됩니다.

 

 

 

 

ResNet model architectures

위의 그림은, ResNet에서 사용한 구조를 모두 정리한 표입니다.

 

 

 

 

4. Experiments

 

 

(실험 파트에서는, 논문을 읽으면서 읽어볼 필요가 있다고 판단되는 검증 내용들을 위주로 정리하였습니다.)

 

 

4.1 ImageNet Classification

 

 

Plain Networks. 첫 번째로, 저자들은 18-layer와 34-layer plain nets을 평가하였습니다.

 

34-layer plain net은 Fig. 3 (middle)에 나와있으며, 18-layer plain net도 유사한 형태를 가진다고 합니다. 

 

상세한 구조는 위에 올려놓은 Table 1을 참고해주시면 됩니다.

 

 

Table 2에 나온 결과를 살펴보면, 34-layer plain net이 18-layer plain net에 비해서 더 높은 validation error를 가진다는 것을 확인할 수 있습니다.

 

원인을 파악하기 위해, Fig. 4 (left)에서 저자들은 학습 동안의 training/validation error를 비교하였습니다.

 

 

 

여기서, 앞에서 언급한 "degradation problem"을 관측할 수 있었습니다. 

 

즉, 18-layer plain network의 solution space가 34-layer plain network의 solution space의 부분집합임에도 불구하고 34-layer plain network가 더 높은 training error를 나타낸다는 것이죠.

 

저자들은 이러한 최적화의 어려움이 vanishing gradient에 의해서 야기되었다고 생각하지 않습니다.

 

plain network는 Batch Normalization에 의해서 학습되었으며, backward propagated gradient가 healthy norm을 가진다는 것을 검증하였기 때문이죠. 

 

 

Residual Networks. 다음으로는 18-layer residual net과 34-layer residual net을 평가하였습니다.

 

 

baselines architectures는 plain net과 동일하고, Fig. 3 (right)에 나온 것처럼 각 3x3 filter 쌍에 shortcut connection만 추가하였습니다.

 

첫 번째 비교에서 (Table 2, Fig 4 right), 모든 shortcut에 identity mapping을 사용하였고, 차원을 증가할 때는 zero-padding을 사용하였습니다. (option A). 

 

따라서, plain network와 비교했을 때 추가적인 parameter가 필요하지 않습니다.

 

Table 2와 Fig. 4를 보고, 세 가지 주요한 관측을 해볼 수 있습니다.

 

첫 번째, 34-layer ResNet이 18-layer ResNet보다 더 낫습니다. 

 

34-layer ResNet은 상당히 더 낮은 trainig error를 나타내고 있고, validation data에 더 일반화 가능합니다.

 

이는 해당 setting에서 degradation problem이 잘 해결되었음을 나타내고, 이를 통해 증가된 depth로부터 accuracy의 증가를 얻을 수 있게 됩니다.

 

두 번째, plain network와 비교했을 때, 34-layer가 top-1 error에서 3.5% 정도 감소하였으며 (Table 2), 성공적으로 training error를 감소시켰습니다. (Fig.4 right vs left)

 

이를 통해 매우 깊은 system에서의 residual learning의 효과성을 검증하였습니다. 

 

마지막으로, 18-layer plain/residual net이 상당히 정확하지만, 18-layer ResNet이 더 빠르게 수렴합니다. (Fig. 4 right vs left)

 

ResNet은 초기 단계에서 더 빠르게 수렴하도록 만들어 최적화를 더 쉽게 만듭니다.

 

 

Identity vs Projection Shortcuts. 저자들은 parameter-free인 identity shortcuts이 학습을 도와준다는 사실을 보였습니다.

 

다음으로는, projection shortcuts(Eqn.(2))에 대해서 알아봅니다.

 

Table 3에서 3가지 option을 비교합니다.

 

(A) 차원을 늘릴 때, zero-padding shortcut을 사용하고, 모든 shortcut은 parameter-free인 경우

 

(B) 차원을 늘릴 때, projection shortcuts을 사용하고, 다른 shortcut은 identity인 경우

 

(C) 모든 shortcut이 projection인 경우

 

 

Table 3은 모든 3가지 option이 plain인 경우보다 상당히 더 좋음을 나타내고 있습니다.

 

A < B < C인데, A/B/C 간의 차이가 크지 않기 때문에, 이는 degradation problem을 해결하는 데 있어서 projection shortcut이 필수적이지 않음을 나타냅니다.

 

따라서, 저자들은 memory/time complexity와 model size를 줄이기 위해서 논문의 나머지 부분에서 option C는 사용하지 않았다고 합니다.

 

 

Deeper Bottleneck Architectures. 다음으로는 ImageNet에서 사용한 deeper net에 대해서 알아봅니다.

 

저자들이 사용할 수 있는 training time에 대한 우려 때문에, building block을 bottleneck design으로 수정했다고 합니다.

 

각 residual function $\mathcal{F}$에 대해서, 2개의 layer 대신 3개의 layer를 쌓은 형태를 사용하였습니다.

 

3개의 layer는 1x1, 3x3, 1x1 convolution이며, 1x1 layer는 차원을 줄이거나 늘리는 역할을 하고, 3x3 layer bottleneck은 줄어든 dimension에 대해 연산을 진행하게 됩니다.

 

Fig. 5는 하나의 예시를 보여주고 있고, 두 디자인은 동일한 time complexity를 가진다고 합니다.

 

왼쪽은 64차원인 경우이고, 오른쪽은 256차원인 경우 이므로, bottleneck design을 통해서 complexity를 줄인 것임을 확인할 수 있습니다.

 

 

parameter-free identity shortcut은 bottleneck architecture에서 특히 중요한데, 만약 Fig. 5 (right)에 있는 identity shortcut을 projection으로 교체한다면, time complexity와 model size가 두배가 되기 때문입니다. 이는 shortcut이 두 개의 고차원끼리 연결되어 있기 때문입니다.

 

따라서, identity shortcut은 bottleneck design을 위해서 더욱 효율적인 모델을 만들어줍니다. 

 

 

50-layer ResNet: 저자들은 34-layer에 있던 2-layer block을 3-layer bottleneck block으로 교체하였고, 이를 통해 50-layer ResNet가 만들어지게 되었습니다.

 

저자들은 차원을 증가시킬 때 option B을 사용하였습니다.

 

101-layer and 152-layer ResNets: 저자들은 101-layer와 152-layer인 ResNet을 3-layer block을 사용해서 만들었습니다.

 

비록 depth는 상당히 증가했지만, 152-layer ResNet은 여전히 Vgg-16/19보다 더 낮은 complexity를 가집니다.

 

 

 

여기까지 ResNet 논문을 정리해 보았습니다.

 

아주 간단한 아이디어를 통해, degradation problem을 해결한 논문으로 매우 유명한 논문이죠.

 

다음 글에서는, ResNet을 코드로 구현한 것을 정리해보도록 하겠습니다.

 

감사합니다.

 

 

이번 글에서는 WGAN 코드를 리뷰해보겠습니다.

 

 

Wasserstein GAN 논문에 대한 내용은 아래의 글에서 보실 수 있습니다.

 

 

cumulu-s.tistory.com/31

 

5. Wasserstein GAN (WGAN) - paper review

안녕하세요. 오늘은 기존의 GAN에서 변형을 준 논문 중 하나인 Wasserstein GAN (WGAN)에 대해서 다뤄보려고 합니다. 논문 주소: arxiv.org/abs/1701.07875 Wasserstein GAN We introduce a new algorithm named W..

cumulu-s.tistory.com

 

 

그럼 시작해보겠습니다.

 

 

transformation = transforms.Compose([
        transforms.Resize(32),
        transforms.ToTensor(),
        transforms.Normalize((0.5,), (0.5,)),
    ])


train_dataset = torchvision.datasets.MNIST(root = '/content/drive/MyDrive/MNIST', train = True, download = True, 
                                            transform = transformation)

print("dataset size: ", len(train_dataset))

trainloader = torch.utils.data.DataLoader(train_dataset, batch_size = BATCH_SIZE, shuffle = True, num_workers = 2)

latent_dim = 100


clip_value = 0.005
n_critic = 5
sample_interval = 400

img_shape = (1, 32, 32)

cuda = True if torch.cuda.is_available() else False

먼저 데이터를 불러와줍니다. 데이터는 MNIST를 사용하였습니다.

 

latent dimension은 100으로 지정하였고, 논문에서 사용된 weight clipping은 0.005로 지정하였습니다. 논문에는 0.01로 지정되어 있었으나, 코드를 돌려보니 좋지 못한 결과가 나와서 이를 줄여서 실험을 진행했습니다.

 

n_critic은 generator보다 critic을 얼마나 더 많이 학습 시킬 것인지를 나타내는 값입니다. 저는 5배 더 많이 학습시키도록 설정하였습니다.

 

그리고 sample_interval은 얼마나 자주 샘플 이미지를 생성해볼 것인지를 결정합니다. 저는 400 배치마다 저장하도록 설정하였습니다.

 

 

# Generator
class Generator(torch.nn.Module):
    def __init__(self, channels):
        super().__init__()
        # Filters [1024, 512, 256]
        # Input_dim = 100
        # Output_dim = C (number of channels)
        self.main_module = nn.Sequential(
            # Z latent vector 100
            nn.ConvTranspose2d(in_channels=100, out_channels=1024, kernel_size=4, stride=1, padding=0),
            nn.BatchNorm2d(num_features=1024),
            nn.ReLU(True),

            # State (1024x4x4)
            nn.ConvTranspose2d(in_channels=1024, out_channels=512, kernel_size=4, stride=2, padding=1),
            nn.BatchNorm2d(num_features=512),
            nn.ReLU(True),

            # State (512x8x8)
            nn.ConvTranspose2d(in_channels=512, out_channels=256, kernel_size=4, stride=2, padding=1),
            nn.BatchNorm2d(num_features=256),
            nn.ReLU(True),

            # State (256x16x16)
            nn.ConvTranspose2d(in_channels=256, out_channels=channels, kernel_size=4, stride=2, padding=1))
            # output of main module --> Image (Cx32x32)

        self.output = nn.Tanh()

    def forward(self, x):
        x = self.main_module(x)
        return self.output(x)

해당 구조는 DCGAN의 구조를 그대로 사용하였습니다.

 

 

class Discriminator(torch.nn.Module):
    def __init__(self, channels):
        super().__init__()
        # Filters [256, 512, 1024]
        # Input_dim = channels (Cx64x64)
        # Output_dim = 1
        self.main_module = nn.Sequential(
            # Image (Cx32x32)
            nn.Conv2d(in_channels=channels, out_channels=256, kernel_size=4, stride=2, padding=1),
            nn.LeakyReLU(0.2, inplace=True),

            # State (256x16x16)
            nn.Conv2d(in_channels=256, out_channels=512, kernel_size=4, stride=2, padding=1),
            nn.BatchNorm2d(512),
            nn.LeakyReLU(0.2, inplace=True),

            # State (512x8x8)
            nn.Conv2d(in_channels=512, out_channels=1024, kernel_size=4, stride=2, padding=1),
            nn.BatchNorm2d(1024),
            nn.LeakyReLU(0.2, inplace=True))
            # outptut of main module --> State (1024x4x4)

        self.output = nn.Sequential(
            nn.Conv2d(in_channels=1024, out_channels=1, kernel_size=4, stride=1, padding=0),
            # Output 1
        )

    def forward(self, x):
        x = self.main_module(x)
        return self.output(x)

critic 부분도 DCGAN과 동일하게 사용하였는데, 차이가 있다면 맨 마지막에 sigmoid를 제거하였습니다.

 

 

 

# Initialize generator and discriminator
generator = Generator(1)
discriminator = Discriminator(1)

if cuda:
    generator.cuda()
    discriminator.cuda()

fixed_noise = torch.randn(16, 100, 1, 1).cuda()


# Optimizers
optimizer_G = torch.optim.RMSprop(generator.parameters(), lr=0.00005)
optimizer_D = torch.optim.RMSprop(discriminator.parameters(), lr=0.00005)

Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor

fixed_noise는 고정된 noise vector를 만들어주고자 생성하였고, optimizer로는 RMSprop을 사용하였습니다.

 

 

batches_done = 0
for epoch in tqdm(range(EPOCHS)):

    D_losses = []
    G_losses = []

    for i, (imgs, _) in enumerate(trainloader):

        # Configure input
        real_imgs = Variable(imgs.type(Tensor))

        # ---------------------
        #  Train Discriminator
        # ---------------------

        optimizer_D.zero_grad()

        # Sample noise as generator input
        z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], latent_dim, 1, 1))))

        # Generate a batch of images
        fake_imgs = generator(z).detach()
        # Adversarial loss
        loss_D = -torch.mean(discriminator(real_imgs)) + torch.mean(discriminator(fake_imgs))

        writer.add_scalar("Train/Wasserstein_estimate", -loss_D.item(), batches_done)

        loss_D.backward()
        optimizer_D.step()

        D_losses.append(-loss_D.item())

        # Clip weights of discriminator
        for p in discriminator.parameters():
            p.data.clamp_(-clip_value, clip_value)

 

이미지를 real_imgs로 가져오고, z라는 변수로 sample noise를 만들어준 다음, 이를 generator에 투입해서 가짜 이미지를 만들어줍니다.

 

그리고 논문에 나왔던대로, Discriminator loss를 계산해줍니다. 

 

단, 논문에서는 gradient ascent를 해주는 식으로 Algorithm이 설계되었기 때문에 우리는 gradient descent를 적용하기 위해 부호를 반대로 바꿔줍니다.

 

그래서 writer.add_scalar를 적용할때는 -loss_D.item()으로 입력되도록 만든 것입니다. 부호가 반대로 들어갔기 때문이죠.

 

그리고 마지막으로는 weight clipping을 해줍니다.

 

 

 # Train the generator every n_critic iterations
        if i % n_critic == 0:

            # -----------------
            #  Train Generator
            # -----------------

            optimizer_G.zero_grad()

            # Generate a batch of images
            gen_imgs = generator(z)
            # Adversarial loss
            loss_G = -torch.mean(discriminator(gen_imgs))

            writer.add_scalar("Train/Generator_loss", loss_G.item(), batches_done // n_critic)

            G_losses.append(loss_G.item())

            loss_G.backward()
            optimizer_G.step()

 

n_critic은 어느정도의 빈도로 generator를 update 해줄 것인지 결정하는 것이라고 앞에서 설명하였는데, 이를 이용해서 batch의 index가 n_critic의 배수일 때만 generator를 학습하도록 만들어줍니다.

 

sample noise인 z를 다시 generator에 투입하고, 이를 통해서 가짜 이미지를 만들어줍니다. 

 

그리고 이를 discriminator에 투입한 값의 평균에 마이너스를 취한 값을 generator loss로 계산해줍니다.

 

이를 통해서 얻게 되는 loss graph는 다음과 같았습니다.

 

 

분명 논문에서는 엄청나게 깔끔하게 하향하는 곡선이 올라와있었는데, 실제로 돌려보니 그렇지는 않았습니다..

 

 

아무래도 hyperparameter들을 조정하거나 해야 더 좋은 성능이 나올 것 같은데... 

 

 

나와있는 수많은 코드들을 직접 돌려보고 해봤는데도 멀쩡히 돌아가는 코드를 못 찾아서 몇일동안 고군분투하다가 이정도에서 마무리하기로 하였습니다.

 

GAN을 향상시킨 모델이라고 들었어서 기대를 많이 했지만, 여전히 생성 모델로 좋은 결과를 만드는 것이 쉽지만은 않네요..

 

 

해당 코드는 제 Github에 올라와있으니, 여기서 전체 코드를 확인하실 수 있습니다.

 

github.com/PeterKim1/paper_code_review/tree/master/5.%20Wasserstein%20GAN(WGAN)

 

PeterKim1/paper_code_review

paper review with codes. Contribute to PeterKim1/paper_code_review development by creating an account on GitHub.

github.com

 

 

 

안녕하세요. 오늘은 기존의 GAN에서 변형을 준 논문 중 하나인 Wasserstein GAN (WGAN)에 대해서 다뤄보려고 합니다.

 

 

논문 주소: arxiv.org/abs/1701.07875

 

Wasserstein GAN

We introduce a new algorithm named WGAN, an alternative to traditional GAN training. In this new model, we show that we can improve the stability of learning, get rid of problems like mode collapse, and provide meaningful learning curves useful for debuggi

arxiv.org

다소 수학적인 내용들을 많이 포함하고 있어, 다음 슬라이드 자료들을 참고했습니다. 

 

 

www.slideshare.net/ssuser7e10e4/wasserstein-gan-i

 

Wasserstein GAN 수학 이해하기 I

이 슬라이드는 Martin Arjovsky, Soumith Chintala, Léon Bottou 의 Wasserstein GAN (https://arxiv.org/abs/1701.07875v2) 논문 중 Example 1 을 해설하는 자료입니다

www.slideshare.net

 

 

그럼 시작해보겠습니다.

 

 

 

1. Introduction

 

 

 

본 연구에서 관심을 가지는 문제는 비지도 학습이다. 

 

대부분, 확률 분포를 학습한다는 것이 무엇을 의미할까?

 

이에 대한 고전적인 정답은 확률 밀도를 학습하는 것이다. 

 

이는 종종 밀도의 parametric family $(P_\theta)_{\theta \in R^d}$을 정의함으로써 이루어지며, 우리 데이터에 대한 likelihood를 최대화하는 parameter를 찾게 된다.

(제 생각에는 이 $P_\theta$라는 것은 어떤 분포의 parameter 일 수도 있고, 아니면 neural network의 가중치가 될 수도 있는 것 같습니다. 정규분포라고 한다면 평균과 표준편차가 parameter가 되겠죠. 그래서 parametric family라는 용어를 사용한 것이 아닐까라는 생각이 듭니다.)

 

즉, 만약 우리가 실제 데이터 $\left\{x^{(i)}\right\}^m_{i=1}$를 가지고 있을 때, 우리는 다음 문제를 해결하는 것이다.

 

만약 실제 데이터 분포 $\mathcal{P}_r$가 밀도를 나타내고, $\mathcal{P}_\theta$가 parametrized density $P_\theta$의 분포라면, 점근적으로, 이 양은 KL divergence $KL(\mathcal{P}_r \parallel \mathcal{P}_\theta)$를 최소화한다. (즉, 실제 데이터 분포와 parameter로 나타내지는 밀도 사이의 거리를 최소화해서 parameter로 실제 데이터 분포에 가깝게 만들어보자는 의미입니다.)

 

이것이 타당하기 위해서, 우리는 model density $P_\theta$가 존재할 필요가 있다. 

 

하지만, 이는 저차원 manifold에 의해서 지지를 받는 분포를 다루는 일반적인 상황에서는 그렇지 않다.

 

model manifold와 실제 분포의 support가 무시할 수 없는 교차점을 가질 가능성은 거의 없으며, 이는 KL distance가 정의되지 않는다는 것을 의미한다. (혹은 단순히 무한하다.)

(support라는 개념은 한국어로는 지지집합이라는 개념인데, 어떤 함수가 존재할 때 함숫값이 0이 되는 정의역의 집합을 의미합니다. 예를 들어서, 함수가 f(x) = x + 1이라면, 지지 집합은 x = -1이 될 것입니다.)

 

 

전형적인 해결책은 model distribution에 noise term을 추가하는 것이다. 

 

이것은 고전적인 머신러닝 문헌에서 묘사되는 사실상 모든 생성 모델들이 noise component를 포함하는 이유이다.

 

가장 간단한 경우에, 모든 예시들을 커버하기 위해 상대적으로 높은 대역폭을 가지는 Gaussian noise를 가정한다.

 

예를 들어, 이미지 생성 모델의 경우에, 이 noise는 샘플의 품질을 저하시키고 뿌옇게 만든다는 사실은 잘 알려져 있다.

 

예를 들어, 최근 논문 [23]에서 likelihood를 최대화할 때 모델에 더해지는 noise의 최적의 standard deviation은 생성된 이미지에서 각 픽셀에 약 0.1이며, 이때 픽셀은 이미 정규화되어 [0, 1] 범위 안에 들어간 상태이다.

 

이는 매우 높은 양의 noise이며, 따라서 논문에서 모델의 샘플을 나타낼 때, 그들은 likelihood number를 보고하는 noise term을 추가하지 않는다.

 

즉, 추가된 noise term은 문제에 대해서 명백하게 부정확하지만, maximum likelihood approach가 작동하도록 하기 위해 필요하다. 

 

 

아마도 존재하지 않는 $\mathcal{P}_r$의 밀도를 추정하는 것 대신에, fixed distribution $p(z)$를 가지는 random variable $Z$를 정의할 수 있으며 이를 어떤 분포 $\mathcal{P}_\theta$를 따르는 샘플을 직접적으로 만드는 parametric function $g_\theta : Z \rightarrow X$ (전형적으로 어떤 종류의 신경망)에 통과시킨다.

 

$\theta$를 다르게 하면서, 우리는 이 분포를 변화시킬 수 있고 실제 데이터 분포 $\mathcal{P}_r$에 가깝게 만들 수 있다.

 

이는 두 가지 측면에서 유용하다.

 

첫 번째로, 밀도와는 다르게, 이 접근법은 저차원의 manifold에 국한된 분포를 표현할 수 있다.

 

두 번째로, 쉽게 샘플을 생성할 수 있는 능력은 밀도의 값을 아는 것보다 더욱 유용하다. (예를 들어, 이미지 super-resolution이나 semantic segmentation에서 input image가 주어졌을 때의 output image의 조건부 분포를 고려할 때)

 

일반적으로, 임의의 고차원 밀도가 주어졌을 때 샘플을 생성하는 것은 연산적으로 어렵다.

 

 

Variational Auto-Encoders (VAEs)와 Generative Adversarial Networks (GANs)는 이러한 접근법으로 잘 알려져 있다.

 

왜냐하면 VAE는 examples의 approximate likelihood에 초점을 두기 때문에, 표준 모델의 한계점을 공유하며 추가적인 noise terms을 조작할 필요가 있다.

 

GANs는 목적 함수의 정의에서 훨씬 더 많은 융통성을 제공하며, Jensen-Shannon과 모든 f-divergence, exotic combinations를 포함한다.

 

반면에, GANs를 학습시키는 것은 [1]에서 이론적으로 연구된 이유로 인해 까다롭고 불안정한 것으로 잘 알려져 있다.

 

 

본 논문에서는 거리나 분산 $\rho(\mathcal{P}_\theta, \mathcal{P}_r)$를 정의하는 다양한 방법에 대해 모델 분포와 실제 분포가 얼마나 가까운지를 측정하는 다양한 방법에 관심을 가진다. 

 

이러한 distance 사이의 가장 근본적인 차이는 확률 분포의 sequence의 convergence에 미치는 영향이다.

 

분포의 sequence $(\mathcal{P}_t)_{t \in N}$는 $\rho(\mathcal{P}_t, \mathcal{P}_\infty)$가 0이 되는 경향이 있는 분포 $\mathcal{P}_\infty$가 존재할 때 수렴하게 되며 이는 distance $\rho$가 얼마나 정확히 정의되는지에 달려있다.

 

비공식적으로, distance $\rho$는 이것이 distribution의 sequence가 수렴하기 더 쉽도록 만들 때 weaker topology를 유발한다.

 

Section 2는 인기있는 확률 거리가 이 점에서 얼마나 다른지 명확히 한다. 

 

 

Parameter $\theta$를 최적화하기 위해서, 물론 mapping $\theta \mapsto \mathcal{P}_\theta$를 연속적으로 만드는 방식으로 우리의 모델 분포 $\mathcal{P}_\theta$를 정의하는 것이 바람직하다.

 

연속성은 parameter의 sequence인 $\theta_t$가 $\theta$에 수렴할 때, distributions $\mathcal{P}_{\theta_t}$ 또한 $\mathcal{P}_\theta$에 수렴한다는 것을 의미한다.

 

하지만, distributions $\mathcal{P}_{\theta_t}$의 수렴이라는 개념은 distribution 간 거리를 계산하는 방법에 의존한다는 사실을 기억하는 것이 필수적이다.

 

이 거리가 약할수록, $\theta$-space에서 $\mathcal{P}_\theta$-space로의 continuous mapping을 정의하는 것이 쉬워지게 되는데, 이는 distribution이 수렴하기가 쉽기 때문이다.

 

Mapping $\theta \mapsto \mathcal{P}_\theta$를 continuous가 되도록 신경을 쓰는 주요한 이유는 다음과 같다.

 

만약 $\rho$가 두 분포 간 우리의 거리 개념이라면, 우리는 continuous loss function $\theta \mapsto \rho(\mathcal{P}_\theta, \mathcal{P}_r)$를 가지고 싶을 것이며, 이는 분포 간 거리인 $\rho$를 사용했을 때 mapping $\theta \mapsto \mathcal{P}_\theta$가 continuous가 되도록 하는 것과 동일하다.

 

 

본 논문의 contribution은 다음과 같다.

 

  • Section 2에서는, 분포를 학습하는 관점에서 주로 사용되는 probability distance와 divergence를 비교하여 Earth Mover (EM) distance가 어떻게 작용하는지에 대한 포괄적인 이론적 분석을 제공한다.
  • Section 3에서는, EM distance의 효율적이고 합리적인 approximation을 최소화하는 Wasserstein-GAN이라고 불리는 형태의 GAN을 정의하고, 대응되는 최적화 문제가 타당하다는 것을 이론적으로 보인다.
  • Section 4에서는, GAN의 주요한 학습 문제를 WGANs이 경험적으로 해결한다는 것을 보인다. 실제로 WGANs을 학습하는 것은 discriminator와 generator 사이의 조심스러운 균형을 유지하는 것이 요구되지 않으며 network architecture의 조심스러운 설계 또한 요구되지 않는다. GANs에서 주로 발생하는 mode dropping phenomenon 또한 매우 줄어든다. WGANs의 가장 주목할만한 실질적 이득은 discriminator를 학습시킴으로써 EM distance를 최적성까지 끊임없이 추정할 수 있는 능력이다. 이러한 학습 curve를 그리는 것은 hyperparameter search와 디버깅에 유용할 뿐만 아니라 관측되는 샘플 품질과 현저하게 상관관계가 있다.

 

 

2. Different Distances

 

 

 

우리는 이제 우리의 notation을 도입한다. 

 

$\mathcal{X}$를 compact metric set (이미지 $[0, 1]^d$의 공간과 같은)이라 하고, $\sum$은 $\mathcal{x}$의 모든 Borel subset의 집합을 나타낸다.

 

(수학적인 용어들이라 내용이 좀 어려운데... 일단 compact라는 용어부터 정리해보겠습니다.

 

compact라는 의미는 경계가 있고 (bounded) 동시에 경계를 포함 (closed)하는 집합을 의미합니다. 적절한 비유를 하자면, '서울시' 라고 하면 서울시의 시 영토가 있을 것이고, 지도상에서 어떤 경계가 존재하는 것을 생각해보시면 될 것 같습니다. 

 

metric은 distance라는 용어로도 불리는 개념으로, 거리함수가 존재하는 것을 의미합니다. 예를 들면 유클리디안 거리가 여기에 해당합니다.

 

Borel 집합은 X 내에서 측정 가능한 집합들을 말합니다. 즉, 확률분포로 확률 값이 계산될 수 있는 집합을 의미합니다. 연속함수의 기댓값을 계산하기 위한 수학적인 최소 조건이라고 합니다.)

 

 

$Prob(\mathcal{X})$은 $\mathcal{X}$에 정의된 probability measures의 공간을 의미한다. (probability measures는 확률 분포랑 동일한 용어입니다.)

 

우리는 이제 두 분포 $\mathcal{P}_r, \mathcal{P}_\theta \in Prob(\mathcal{X})$ 사이의 거리와 divergence에 대해서 정의할 수 있다.

 

 

  • Total Variation (TV) distance

 

  • Kullback-Leibler (KL) divergence

 

$\mathcal{P}_r$와 $\mathcal{P}_g$는 완전히 연속이라고 가정되며, 그러므로 $\mathcal{X}$에 정의된 동일한 measure $\mu$에 대해서 밀도를 인정한다. 

 

KL divergence는 유명하게 비대칭이며 $P_g(x) =0, P_r(x) > 0$인 point가 존재할 때 무한이다.

 

  • Jensen-Shannon (JS) divergence

 

$\mathcal{P}_m$은 $(\mathcal{P}_r + \mathcal{P}_g)/2$이다. 이 divergence는 대칭이며 우리가 $\mu = P_m$으로 선택할 수 있기 때문에 항상 정의된다.

 

 

  • Earth-Mover (EM) distance or Wasserstein-1

$\prod(\mathcal{P}_r, \mathcal{P}_g)$는 주변 확률 분포가 각각 $\mathcal{P}_r$와 $\mathcal{P}_g$인 모든 결합 분포 $\gamma(x, y)$의 집합을 나타낸다.

 

직관적으로, $\gamma(x, y)$는 분포 $\mathcal{P}_r$를 분포 $\mathcal{P}_g$로 변형시키기 위해서 얼마나 많은 "질량"을 $x$에서 $y$로 운송해야 하는지를 나타낸다. EM distance는 최적 운송 계획의 cost이다. 

 

 

다음 예시는 확률 분포의 간단한 sequence가 EM distance 하에서는 얼마나 명백하게 수렴하고 다른 distance와 divergence 하에서는 수렴하지 않는지를 나타낸다.

 

 

Example 1 (수평선을 학습하는 경우). $Z \sim U[0, 1]$는 unit interval 하에서의 균일 분포이다. $P_0$는 $(0, Z) \in R^2$ (0은 x 축이고 random variable $Z$는 y축)의 분포라고 가정하고, 원점을 통과하는 직선의 수직선이다.

 

$g_\theta(z) = (\theta, z)$라고 하고 $\theta$는 single real parameter이다. 

 

 

 

$\theta_t -> 0$일 때, sequence ($\mathcal{P}_{\theta_t})_{t \in N}$ 는 EM distance 하에서 $\mathcal{P}_0$으로 수렴하지만, JS, KL, reverse KL, TV divergence 하에서는 수렴하지 않는다. Figure 1는 EM과 JS distance의 경우에 대해 이를 나타낸다.

 

 

 

Figure 1을 살펴보면, JS divergence의 경우 대부분의 케이스에서 값이 같으므로, 적절한 gradient 값을 얻기가 어렵습니다. 반면에, EM distance의 경우 대부분의 케이스에서 유의미한 gradient 값을 얻을 수 있게 되죠.

 

Example 1은 EM distance에 대해서 gradient descent를 진행함으로써 저차원의 manifold에 대한 확률 분포를 학습할 수 있는 케이스를 우리에게 제공한다. 이는 다른 distance나 divergence를 가지고는 될 수 없는데, 이는 resulting loss function이 연속적이지 않기 때문이다. 

 

 

Wasserstein distance가 JS distance에 비해서 훨씬 약하기 때문에, 가벼운 가정 하에서도 $W(\mathcal{P}_r, \mathcal{P}_\theta)$가 $\theta$에 대한 연속형 loss function인지 아닌지를 물어볼 수 있다. 이는 진실이며, 이제 이를 증명한다.

 

 

 

 

2번에 나와있는 regularity assumption 1도 가져와 보겠습니다.

 

 

사실 모든 내용을 정확하게 이해하기란 어렵지만, 일단 제가 이해했던 내용을 조금 설명해보자면...

 

먼저 Assumption 1.에서 'locally Lipschitz'라고 하는 내용이 있습니다. 이것에 대해서 먼저 알아보겠습니다.

 

구글에 locally Lipschitz라고 검색하면, 다음 페이지가 검색이 될 텐데요.

 

en.wikipedia.org/wiki/Lipschitz_continuity

 

Lipschitz continuity - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Strong form of uniform continuity For a Lipschitz continuous function, there exists a double cone (white) whose origin can be moved along the graph so that the whole graph always stays

en.wikipedia.org

 

여기 내용을 보면 다음과 같이 적혀 있습니다. 

 

Intuitively, a Lipschitz continuous function is limited in how fast it can change: there exists a real number such that, for every pair of points on the graph of this function, the absolute value of the slope of the line connecting them is not greater than this real number; the smallest such bound is called the Lipschitz constant of the function.

 

Lipschitz function이라는 것은 얼마나 빨리 변화할지가 제한된 함수라는 얘깁니다. 2차원에서 생각해보면, 임의의 두 점을 이었을 때 이 직선의 기울기가 바로 변화율이 되죠? 이것이 제한된다는 얘기죠.

 

그리고 그 bound의 가장 작은 값이 Lipschitz constant라는 것이고요.

 

즉 어떤 함수가 Lipschitz constant라는 값보다는 항상 변화율이 작아야 한다는 그런 말이 됩니다.

 

Assumption 1은 이 Lipschitz constant의 기댓값이 무한대보다 작은 경우 g라는 함수가 다음 가정을 만족한다고 적혀있네요. 

 

변화율을 어느 특정 값보다 작게 한다는 것은... 아무래도 Gradient가 과하게 커진다거나 그런 상황을 방지하는 거겠죠?

 

쓸모 있는 gradient를 얻을 수 있게 한다고 생각하면 될까요... 아무튼 이 정도로 생각해볼 수 있을 것 같습니다.

 

 

 

$z$에 대한 prior $p(z)$에 대해서 이에 대한 distance의 기댓값이 무한대보다 작으면, 아까 얘기했던 locally lipschitz 조건을 만족시키게 되고, Wasserstein distance가 모든 곳에서 연속이고 거의 모든 곳에서 미분 가능하다... 이런 얘기입니다.

 

이 모든 것들은 EM이 적어도 JS divergence에 비해서 우리 문제에 대해 훨씬 더 민감한 cost function이라는 것을 보인다.

 

다음 theorem은 이러한 distance와 divergence에 의해서 유도되는 위상의 상대적 강함을 묘사하며, KL이 가장 강력하고, 그다음으로는 JS, TV, EM이 가장 약하다.

 

 

이는 KL, JS, TV distance가 저차원의 manifold에 의해서 지지를 받는 분포를 학습할 때 민감한 cost function이 아니라는 사실을 강조한다. 하지만 EM distance는 이러한 환경에서 민감하다. 

 

 

3. Wasserstein GAN

 

 

Theorem 2는 최적화가 이루어질 때 Wasserstein distance가 JS divergence에 비해서 더 좋은 특성을 가진다는 사실을 가리킨다.

 

하지만, EM distance의 하한 중 가장 큰 값은 매우 계산하기가 어렵다. 반면에, Kantorovich-Rubinstein duality는 다음을 설명한다.

 

 

그리고 이 식을 만약 우리가 어떤 $K$에 대해서 $K$-Lipschitz를 만족하는 parameterized family of functions을 가지고 있다면 다음과 같이 변경할 수 있다고 합니다.

 

 

그리고 이 문제에 대한 솔루션 $f : \mathcal{X} -> R$을 다음과 같이 구할 수 있다.

 

 

이제 equation (2)에 있던 최대화 문제를 해결하는 함수 $f$를 찾는 문제에 도달한다.

 

이를 대략 근사하기 위해서, 우리가 할 수 있는 것은 compact space $\mathcal{W}$에 놓여있는 가중치 $w$를 파라미터로 가지는 neural network를 학습시키는 것으로, 그러고 나서 $E_{z \sim p(z)} [\triangledown_\theta f_w (g_\theta(z)) ]$을 통해서 backprop을 한다. 이 과정은 일반적인 GAN을 가지고 수행한다.

 

$\mathcal{W}$이 compact라는 사실은 모든 functions $f_w$가 개별 가중치가 아닌 $\mathcal{W}$에만 의존하는 일부 $K$에 대한 K-Lipschitz일 것이라고 암시하므로 (2)를 관련이 없는 scaling factor와 critic $f_w$의 용량까지 근사한다.

 

Parameters $w$가 compact space에 놓이도록 하기 위해서(어떤 경계 안에 들어오게 하기 위해서), 우리가 할 수 있는 간단한 어떤 것은 각 gradient update 이후에 가중치들을 fixed box(예를 들어 $\mathcal{W} = [-0.01, 0.01]^l$)에 고정시키는 것이다.

 

Wasserstein Generative Adversarial Network (WGAN) 절차는 Algorithm 1에 나와있다.

 

Weight clipping은 Lipschitz constraint를 강제하기 위해서 명백하게 끔찍한 방법이다.

 

만약 clipping parameter가 크면, 어떤 가중치는 이들의 limit까지 도달하는데 오랜 시간이 걸릴 수 있으며, critic을 최적성까지 학습시키기 어렵게 만든다.

 

만약 clipping이 작으면, 이는 layer의 수가 적을 때 혹은 batch normalization이 사용되지 않았을 때 vanishing gradient로 이끌기 쉬울 수 있다.

 

 

우리는 거의 차이가 없는 간단한 변형을 가지고 실험했으며, 단순성과 이미 좋은 성능을 내는 것 때문에 weight clipping을 고수하기로 했다.

 

하지만, 우리는 신경망 환경에서 Lipschitz constraints를 강제하는 주제를 추가적인 연구 주제로 남겨둔다.

 

 

 

해당 표가 WGAN의 전체적인 절차를 담고 있습니다. 추후 code review에서 이 내용이 어떻게 코드로 구현되는지도 알아볼 예정입니다.

 

EM distance가 연속적이고 미분 가능하다는 사실은 우리가 critic을 최적성까지 훈련시킬 수 있다는 사실을 의미한다.

 

논거는 간단한데, 우리가 더 많이 critic을 학습시킬수록 더 신뢰할 만한 Wasserstein의 gradient를 우리가 얻게 되며, 이는 Wasserstein의 거의 모든 곳에서 미분 가능하다는 사실에 의해 실제로 유용하다.

 

JS의 경우에, discriminator가 더 나은 gradients을 얻을수록 더 신뢰할 만한 gradient을 얻게 되지만, JS가 locally saturated이기 때문에 true gradient는 0이고 우리가 figure 1과 [1]의 Theorem 2.4에서 볼 수 있듯이 vanishing gradient을 얻게 된다.

 

Figure 2에서 이 개념의 증명을 볼 수 있으며, 여기서 우리는 GAN discriminator와 WGAN critic을 최적성까지 학습하였다.

 

 

 

Discriminator는 fake와 real 사이를 구별하기 위해서 매우 빠르게 학습하며, 예상되듯이 신뢰할 만한 gradient information을 제공하지 못한다.

 

하지만 critic은 saturate 되지 않으며 모든 곳에서 현저하게 명백한 gradient를 제공하는 linear function으로 수렴한다.

 

우리가 가중치를 제한한다는 사실은 공간의 서로 다른 부분에서 함수의 가능한 성장을 최대 선형으로 제한하며, 이는 최적의 critic이 이러한 행동을 하도록 강제한다.

 

더 중요한 점은, 우리가 critic을 최적성까지 학습할 수 있다는 사실은 우리가 학습할 때 modes가 붕괴되는 것을 불가능하게 만든다.

 

 

 

4. Empirical Results

 

 

우리는 Wasserstein-GAN algorithm을 사용하여 image generation에 대해서 실험을 수행하였으며 표준적인 GAN에서 사용되는 formulation에 비해서 이를 사용하는 것에 대한 상당한 실제적 이점이 있음을 보인다.

 

우리는 두 가지 주요한 이점을 주장한다.

 

  • Generator의 수렴과 샘플 품질과 상관관계가 있는 의미 있는 loss metric
  • 최적화 프로세스의 향상된 안정성

 

 

4.1 Experimental Procedure

 

 

 

실험은 Image generation에 대해서 수행되었고, 학습하려고 하는 target distribution은 LSUN-Bedrooms dataset을 사용했다. baseline 비교는 DCGAN이 사용되었고, 이는 표준적인 GAN procedure를 사용하여 학습되었다.

 

생성된 샘플은 64x64 pixel의 3 채널 이미지이며, 실험에 사용된 hyperparameter는 Algorithm 1에서 나타난 그대로 사용하였다.

 

 

4.2 Meaningful loss metric

 

 

WGAN algorithm은 각 generator update (Algorithm 1에서의 10번째 줄) 이전에 critic $f$를 상대적으로 잘 학습하도록 시도하기 때문에, 이 지점에서의 loss function은 우리가 $f$의 Lipschitz constant를 제약하는 방법과 관련되어 있는 constant factor까지의 EM distance의 추정치이다.

 

우리의 첫 번째 실험은 얼마나 이 추정치가 생성된 샘플의 품질과 상관관계가 있는지를 나타낸다. 

 

Convolutional DCGAN architecture 외에, generator 혹은 generator와 critic 모두를 4-layer ReLU-MLP with 512 hidden units로 교체하여 실험을 진행하였다.

 

Figure 3는 모든 3개의 architecture의 WGAN training 동안의 EM distance의 WGAN estimate의 변화를 나타낸다. 이 그림들은 생성된 샘플의 시각적 품질과 이들의 그래프가 상관관계가 있음을 명확하게 보여준다.

 

 

 

대조적으로, Figure 4는 GAN training 동안의 JS distance의 GAN estimate의 변화를 보여준다. 

 

GAN training 동안에, discriminator는 이 값을 최대화하도록 학습된다.

 

 

 

이 양은 명백하게 샘플 품질과 상관관계가 없다. 이는 Figure 4에서 확인할 수 있다.

 

 

 

마지막으로, 높은 learning rate를 사용하거나 critic에 Adam과 같은 momentum based optimizer를 사용할 때 WGAN 학습이 불안정해질 수 있다.

 

Critic에 사용되는 loss가 nonstationary 이므로, momentum 기반의 방법론들은 더 안 좋게 perform 하는 것으로 보인다.

 

우리는 momentum이 잠재적인 원인이라고 파악하였는데, 이는 loss가 상승하고 샘플이 악화될 때 Adam step과 gradient 간의 cosine 값이 일반적으로 마이너스로 변하기 때문이다. 

 

이 cosine 값이 마이너스인 유일한 곳은 이러한 불안정한 상황들이었다.

 

그러므로 우리는 nonstationary problem에서도 잘 작동하는 것으로 알려져 있는 RMSProp을 사용하였다.

 

 

4.3 Improved stability

 

 

 

WGAN의 이점 중 하나는 critic을 최적성까지 학습하도록 해준다는 것이다.

 

critic이 완전히 학습되었을 때, 이는 다른 신경망인 generator가 학습될 수 있도록 하는 loss를 제공한다.

 

이는 우리가 더 이상 generator와 discriminator의 capacity를 적절하게 균형 맞출 필요가 없다는 것을 의미한다.

 

critic이 더 잘할수록, generator이 학습하는 데 사용되는 더 높은 gradient를 제공한다.

 

 

우리는 generator에 사용되는 구조를 변화시켰을 때 GAN보다도 WGAN이 더 강건하다는 것을 확인했다.

 

우리는 이를 3개의 generator 구조를 실험해서 확인하였다.

 

(1) convolutional DCGAN generator / (2) convolutional DCGAN generator without batch normalization with 512 hidden units / (3) 4-layer ReLU-MLP with 512 hidden units.

 

마지막 두 개는 GAN을 이용했을 때 매우 안 좋은 성능을 내는 것으로 알려져 있다. 

 

우리는 WGAN critic이나 GAN discriminator로 convolutional DCGAN 구조를 사용했다.

 

Figure 5, 6, 7는 3가지 구조를 이용해서 WGAN과 GAN을 사용하였을 때 생성된 샘플을 보여준다.

 

 

 

5. Related works

 

생략

 

 

 

 

6. Conclusion

 

 

우리는 전통적인 GAN training을 대체하는 WGAN이라는 알고리즘을 도입했다.

 

새로운 모델에서, 학습의 안정성이 향상될 수 있고 mode collapse와 같은 문제들이 제거되며, debugging이나 hyperparameter search에 유용한 유의미한 학습 곡선을 제공한다는 것을 보였다.

 

추가로, 우리는 최적화 문제가 건전함을 보였으며, 분포 간의 다른 거리와의 깊은 연관성을 강조하는 광범위한 이론적인 논의도 제공되었다.

 

 

 

 

수학적인 내용이 많이 들어가 있는 논문이라, 완벽하게 이해하는 것이 불가능했습니다.

 

모델을 이용하는 입장에서 기억해야 할 점이라고 한다면, 먼저 loss가 이미지의 품질과 상관관계를 가지고 있기 때문에 loss 값을 보고서 학습이 잘 진행되고 있는지 파악하기가 쉽다는 점이 있을 것 같습니다.

 

논문의 그림에서 나온 대로, 점점 loss가 줄어들고 이에 따라서 이미지의 품질이 좋아지는 것을 확인하면 학습이 잘 이루어지고 있다는 것을 파악할 수 있겠죠.

 

그리고 기존 GAN의 경우 Generator와 Discriminator가 서로 균형을 이루면서 학습이 진행되어야만 하지만, WGAN의 경우는 critic을 최대한으로 최적 상태까지 학습시키는 것을 통해서 generator에게 더 좋은 gradient를 제공한다는 점에서 학습의 메커니즘 차이가 있다 정도를 기억해두면 좋을 것 같습니다.

 

WGAN는 여기까지 정리하겠습니다.

 

 

 

 

저번 시간에 review 한 DCGAN 논문의 code reivew입니다.

 

 

DCGAN 논문에 대한 세부사항이 궁금하시다면, 다음 주소에서 확인하실 수 있습니다.

 

cumulu-s.tistory.com/28

 

4. Unsupervised Representation learning with Deep Convolutional Generative Adversarial Networks(DCGAN) - paper review

오늘은 Generative Adversarial networks에 Convolutional neural network를 섞은 모델인 DCGAN에 대해서 리뷰를 해보겠습니다. original paper : arxiv.org/abs/1511.06434 Unsupervised Representation Learning..

cumulu-s.tistory.com

 

이번 글에서는 DCGAN 논문에 나온 정보를 바탕으로, 실제로 DCGAN 모델을 짜 보고 결과를 확인해보겠습니다.

 

 

시작합니다.

 

 

 

 

 

from torch.utils.tensorboard import SummaryWriter
import os
import random
import torch
import torch.nn as nn
import torch.optim as optim
import torch.utils.data
import torchvision.datasets
import torchvision.transforms as transforms
import torchvision.utils as vutils
import datetime
import shutil
import matplotlib.pyplot as plt
import numpy as np
from torch.autograd import Variable
import math
from tqdm.auto import tqdm

먼저 필요한 패키지들을 불러옵니다.

 

 

current_time = datetime.datetime.now() + datetime.timedelta(hours= 9)
current_time = current_time.strftime('%Y-%m-%d-%H:%M')

saved_loc = os.path.join('/content/drive/MyDrive/DCGAN_Result', current_time)
if os.path.exists(saved_loc):
    shutil.rmtree(saved_loc)
os.mkdir(saved_loc)

image_loc = os.path.join(saved_loc, "images")
os.mkdir(image_loc)
weight_loc = os.path.join(saved_loc, "weights")
os.mkdir(weight_loc)

print("결과 저장 위치: ", saved_loc)
print("이미지 저장 위치: ", image_loc)
print("가중치 저장 위치: ", weight_loc)

writer = SummaryWriter(saved_loc)

random.seed(999)
torch.manual_seed(999)

current_time은 Google colab을 사용하면 시간이 9시간 밀려서 현재 시간을 찍기 위해 만들어진 것이고,

 

os.mkdir를 이용해서 결과를 저장할 폴더를 만들어줍니다. 

 

SummaryWriter는 tensorboard에 학습 결과를 찍기 위해 만들어줍니다.

 

결과를 다시 그대로 볼 수 있도록, random seed와 manual seed를 따로 주었습니다.

 

 

transformation = transforms.Compose([
        transforms.Resize(64),
        transforms.ToTensor(),
        transforms.Normalize((0.5,), (0.5,)),
    ])


train_dataset = torchvision.datasets.FashionMNIST(root = '/content/drive/MyDrive/Fashion_MNIST', train = True, download = True, 
                                            transform = transformation)

print("dataset size: ", len(train_dataset))

trainloader = torch.utils.data.DataLoader(train_dataset, batch_size = BATCH_SIZE, shuffle = True, num_workers = 2)


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

데이터셋으로는 FashionMNIST를 들고 오고, 논문에서 64x64로 실험을 했으므로 이를 그대로 사용하기 위해 Resize를 통해서 64x64의 이미지로 만들어줍니다. (제 Github에는 32x32로 실험된 결과들도 있습니다.) 

 

 

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

저는 GPU 연산을 사용하기 때문에, cuda 설정을 해줍니다.

 

 

 

# Generator
class Generator(torch.nn.Module):
    def __init__(self, channels):
        super().__init__()
        # Filters [1024, 512, 256, 128]
        # Input_dim = 100
        # Output_dim = C (number of channels)
        self.main_module = nn.Sequential(
            # Z latent vector 100
            nn.ConvTranspose2d(in_channels=100, out_channels=1024, kernel_size=4, stride=1, padding=0),
            nn.BatchNorm2d(num_features=1024),
            nn.ReLU(True),

            # State (1024x4x4)
            nn.ConvTranspose2d(in_channels=1024, out_channels=512, kernel_size=4, stride=2, padding=1),
            nn.BatchNorm2d(num_features=512),
            nn.ReLU(True),

            # State (512x8x8)
            nn.ConvTranspose2d(in_channels=512, out_channels=256, kernel_size=4, stride=2, padding=1),
            nn.BatchNorm2d(num_features=256),
            nn.ReLU(True),

            # State (256x16x16)
            nn.ConvTranspose2d(in_channels=256, out_channels=128, kernel_size=4, stride=2, padding=1),
            nn.BatchNorm2d(num_features=128),
            nn.ReLU(True),

            # State (128x32x32)
            nn.ConvTranspose2d(in_channels=128, out_channels=channels, kernel_size=4, stride=2, padding=1))
            # output of main module --> Image (Cx64x64)

        self.output = nn.Tanh()

    def forward(self, x):
        x = self.main_module(x)
        return self.output(x)

다음으로는 Generator model을 살펴보겠습니다.

 

이는 논문에 나온 구조를 그대로 따라서 만든 거라고 보시면 되겠습니다.

 

 

 

 

# Discriminator
class Discriminator(torch.nn.Module):
    def __init__(self, channels):
        super().__init__()
        # Filters [128, 256, 512, 1024]
        # Input_dim = channels (Cx64x64)
        # Output_dim = 1
        self.main_module = nn.Sequential(
            # Image (Cx64x64)
            nn.Conv2d(in_channels=channels, out_channels=128, kernel_size=4, stride=2, padding=1),
            nn.LeakyReLU(0.2, inplace=True),

            # State (128x32x32)
            nn.Conv2d(in_channels=128, out_channels=256, kernel_size=4, stride=2, padding=1),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(0.2, inplace=True),

            # State (256x16x16)
            nn.Conv2d(in_channels=256, out_channels=512, kernel_size=4, stride=2, padding=1),
            nn.BatchNorm2d(512),
            nn.LeakyReLU(0.2, inplace=True),

            # State (512x8x8)
            nn.Conv2d(in_channels=512, out_channels=1024, kernel_size=4, stride=2, padding=1),
            nn.BatchNorm2d(1024),
            nn.LeakyReLU(0.2, inplace=True))
            # outptut of main module --> State (1024x4x4)

        self.output = nn.Sequential(
            nn.Conv2d(in_channels=1024, out_channels=1, kernel_size=4, stride=1, padding=0),
            # Output 1
            nn.Sigmoid())

    def forward(self, x):
        x = self.main_module(x)
        return self.output(x)

다음으로는 discriminator 구조를 살펴보겠습니다.

 

이는 generator의 구조를 그대로 가져오되, 반대로 만든 거라고 생각하시면 되겠습니다.

 

즉, (batch, 1, 64, 64)의 이미지를 받아서 convolution 연산을 거듭하면서 channel은 늘리고 가로 세로 사이즈는 2배씩 줄입니다.

 

마지막으로는 (batch, 1024, 4, 4)에서 (batch, 1, 1, 1) 형태로 만들어주면서 input으로 받은 이미지가 진짜인지 가짜인지에 대한 값이 나오도록 만들어줍니다. 그래서 맨 마지막의 activation function은 Sigmoid가 사용됩니다.

 

그리고 이전 모델과 다른 점이라면, layer 사이에 사용되는 activation function은 LeakyReLU를 사용해줍니다. 이는 논문에서 언급된 사항이죠. 

 

마찬가지로 BatchNorm은 layer마다 꼭 사용해줘야 하고요.

 

netG = Generator(1).to(device)
netD = Discriminator(1).to(device)

criterion = nn.BCELoss()

# To see fixed noise vector's change
fixed_noise = torch.randn(16, 100, 1, 1, device=device)
real_label = 1
fake_label = 0

# setup optimizer
optimizerD = optim.Adam(netD.parameters(), lr=0.0002, betas=(0.5, 0.999))
optimizerG = optim.Adam(netG.parameters(), lr=0.0002, betas=(0.5, 0.999))

다음으로는 우리가 loss를 계산할 때 사용하게 될 Binary Cross entropy 함수를 미리 만들어줍니다.

 

그리고 fixed_noise라는 이름으로 고정된 noise variable을 만들어줍니다.

 

이는 고정된 noise variable을 이용해서 점점 DCGAN이 학습을 거듭하며 어떤 식으로 이미지를 다시 구현하는지를 알아보기 위함입니다.

 

실제 이미지는 label이 1이고, 가짜 이미지는 label이 0이므로, 이를 변수에 저장해줍니다.

 

마찬가지로 논문에서 나온 대로 Adam optimizer를 사용해주고, learning rate는 0.0002로 지정하며 beta는 0.5로 지정해줍니다.

 

 

for epoch in tqdm(range(EPOCHS)):

    D_losses = []
    G_losses = []
    for i, (images, _) in enumerate(trainloader):


        z = torch.rand((images.size(0), 100, 1, 1))
        real_labels = torch.ones(images.size(0))
        fake_labels = torch.zeros(images.size(0))

        images, z = Variable(images).to(device), Variable(z).to(device)
        real_labels, fake_labels = Variable(real_labels).to(device), Variable(fake_labels).to(device)

다음으로는 정해진 에폭만큼 반복해서 돌면서, 학습이 진행되는 부분입니다.

 

epoch 별로 평균 Discriminator loss와 Generator loss를 계산해주기 위해서, D_losses와 G_losses를 지정해줍니다.

 

그리고 dataloader를 사용해서 FashionMNIST data를 받아오는 for문이 안에 들어갑니다.

 

z라는 변수로 랜덤한 noise를 100차원으로 만들어주고, 이를 gpu로 올려줍니다.

그리고 실제 데이터에 대해서는 real_label을, 가짜 데이터에는 fake_label을 줘야하니 이것도 만들어줍니다.

 

 

############################
        # (1) Update D network: maximize log(D(x)) + log(1 - D(G(z)))
        ###########################

        outputs = netD(images)
        d_loss_real = criterion(outputs.view(-1), real_labels)
        real_score = outputs

        z = Variable(torch.randn(images.size(0), 100, 1, 1)).to(device)
        fake_images = netG(z)
        outputs = netD(fake_images)
        d_loss_fake = criterion(outputs.view(-1), fake_labels)
        fake_score = outputs

        d_loss = d_loss_real + d_loss_fake
        netD.zero_grad()
        d_loss.backward()
        optimizerD.step()

        D_losses.append(d_loss.item())

 

실제 이미지를 Discriminator에 통과시켜서 나온 결과를 output으로 저장하고, 이것과 실제 label인 1과 Binary Cross Entropy를 계산해서 d_loss_real를 계산해줍니다.

 

그리고 새롭게 random noise를 만든다음, 이를 Generator에 통과시켜서 이미지를 생성합니다. 이를 Discriminator에 넣어서 얼마나 진짜 같은지를 계산하고, 이를 fake label인 0과 비교하게 만들어줍니다.

 

real 이미지에서 계산된 loss와 fake 이미지에서 계산된 loss를 더해서, 이를 최종 loss로 계산하고, backprop 해줍니다.

 

 

 

 ############################
        # (2) Update G network: maximize log(D(G(z)))
        ###########################

        z = Variable(torch.randn(images.size(0), 100, 1, 1)).to(device)

        fake_images = netG(z)
        outputs = netD(fake_images)
        g_loss = criterion(outputs.view(-1), real_labels)

        netD.zero_grad()
        netG.zero_grad()
        g_loss.backward()
        optimizerG.step()

        G_losses.append(g_loss.item())

        writer.add_scalars('Train/loss per batch', {'Discriminator loss' : d_loss.item(),
                                     'Generator loss' : g_loss.item()}, i + epoch * (round(len(trainloader.dataset) / BATCH_SIZE)))

 

다음은 Generator를 업데이트 하는 부분입니다.

 

random noise를 만들어주고, 이를 Generator에 넣어서 가짜 이미지를 만들어줍니다.

 

그리고 이를 Discriminator에 넣어서 얼마나 진짜 같은지를 계산합니다.

 

이를 real label과 비교하여 loss를 계산하고, backprop 해줍니다.

 

 

if i % 100 == 0:
            print('[%d/%d][%d/%d] D_loss: %.8f, G_loss: %.8f' 
              % (epoch, EPOCHS, i, len(trainloader),
                 d_loss.item(), g_loss.item()))
            

            fake = netG(fixed_noise)
            fake = fake.mul(0.5).add(0.5)
            grid = vutils.make_grid(fake.cpu())
            vutils.save_image(grid,
                    '%s/fake_samples_epoch_%03d_idx_%03d.png' % (image_loc, epoch, i))

batch index가 100의 배수일 때마다 Discriminator loss와 Generator loss, D(x) 값, D(G(z))의 값들을 출력해줍니다.

 

그리고 이전에 설정했던 고정된 noise variable을 Generator에 넣은 다음 생성되는 이미지를 image_loc의 위치에 저장해줍니다.

 

 

vutils.save_image 코드로 인해서, 다음과 같이 epoch과 batch index에 따라 다음과 같이 저장됨을 확인할 수 있습니다.

 

 

torch.save(netG.state_dict(), '%s/netG_epoch_%d.pth' % (weight_loc, epoch))
    torch.save(netD.state_dict(), '%s/netD_epoch_%d.pth' % (weight_loc, epoch))

    D_loss_epoch = torch.mean(torch.FloatTensor(D_losses))
    G_loss_epoch = torch.mean(torch.FloatTensor(G_losses))
    writer.add_scalars('Train/loss per epoch', {'Average Discriminator loss per epoch' : D_loss_epoch.item(),
                                 'Average Generator loss per epoch' : G_loss_epoch.item()}, epoch)

 

나머지 코드는 가중치 저장하고, loss를 계산하는 부분이니 넘어가겠습니다.

 

 

다른 논문 리뷰에서는 writer.add_scalar를 사용했는데, writer.add_scalars는 하나의 그림에 여러 개의 그림을 한 번에 그릴 때 씁니다. 즉, 그래프는 한 개지만 Discriminator loss와 generator loss를 다 표현하고 싶을 때 사용하는 것이죠.

 

맨 마지막에는 Summarywriter를 닫아줘야 하므로 writer.close()를 해줍니다.

 

 

 

 

 

위 코드를 통해서 얻은 loss 그래프입니다. 왼쪽은 batch 별 loss, 오른쪽은 epoch 별 loss 입니다.

 

 

 

 

 

 

DCGAN을 통해서 만들어낸 이미지들을 가지고 gif로 만든 것입니다.

 

 

마지막으로, 논문에서 나왔던 latent space에 대해서 실험해본 내용을 보여드리고 마무리하도록 하겠습니다.

 

 

 

논문에서 6.1에 해당하는 부분인데요. latent space에서 interpolation을 진행해서 이미지가 어떻게 변하는지, sharp transition이 일어나는지 확인해본 내용입니다.

 

 

 

임의로 (1, 100) 짜리 Gaussian noise variable을 두 개 만든 뒤, 두 지점을 interpolation 해서 얻어낸 그림입니다.

 

왼쪽은 구두이고, 이것이 점점 변하면서 바지가 되는 모습을 볼 수 있습니다.

 

이는 Gaussian noise 값에 따라서 다른 모습들도 만들어낼 수 있습니다.

 

 

 

다른 random 값을 주어서, 다음과 같은 이미지도 얻을 수 있었습니다.

 

두 번째 이미지의 경우, 바지에서 상의로 변하는 모습을 볼 수 있습니다.

 

이렇게 점차적으로 변하는 모습을 확인할 수 있다면, 단순히 training image를 기억하는 것이 아닌 manifold를 학습했다고 보는 것 같습니다.

 

 

여기까지 해서 DCGAN 논문을 마무리 지으려고 합니다.

 

 

오늘 글에서 살펴본 모든 코드는 제 Github에서 확인하실 수 있습니다.

 

github.com/PeterKim1/paper_code_review

 

PeterKim1/paper_code_review

paper review with codes. Contribute to PeterKim1/paper_code_review development by creating an account on GitHub.

github.com

 

 

+ Recent posts