안녕하세요. 

 

 

오늘은 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

 

 

 

 

안녕하세요. 오늘은 torch.nn.ConvTranspose2d 라고 하는 모듈에 대해서 알아보도록 하겠습니다.

 

 

용어로는 Deconvolution이나 fractionally-strided convolution이라고 불리는 작업을 수행합니다.

 

 

해당 작업이 무엇인지를 자세하게 다룬 자료들은 이미 너무 많이 있어 생략하고, 실제로 연산되는 결과를 위주로 살펴보겠습니다.

 

 

이 모듈은 Deep Convolutional GAN을 구현할 때 주로 사용됩니다.

 

 

그럼 시작해보겠습니다.

 

 

 

분석 코드

 

 

import torch
import torch.nn as nn

# sample values.
test_input = torch.Tensor([[[[1, 2, 3], [4, 5, 6]]]])
print("input size: ", test_input.shape)
print("test input: ", test_input)

 

먼저, pytorch를 이용해서 실험해 볼 것이기 때문에 모듈을 불러오고요.

 

input으로 들어갈 tensor를 만들어줍니다.

 

사이즈가 작으면서도 직관으로 이해해보기 쉽도록 다음과 같이 만들었습니다.

 

굳이 4차원으로 만드는 이유는, pytorch에서는 기본적으로 4차원 input만을 지원하기 때문입니다.

 

따라서, 실제로 값이 있는 것은 1부터 6까지로 해서 2 x 3 형태이지만, shape를 맞추기 위해 4차원으로 구성되었다고 보시면 될 것 같습니다.

 

# sample model. It has nn.ConvTranspose2d(1, 3, 4, 1, 0, bias = False)
# First parameter = Channels of input (=1)
# Second parameter = Channels of output (=3)
# Third parameter = Kernel size (=4)
# Fourth parameter = stride (=1)
# fifth parameter = padding (=0)
class sample(nn.Module):
  def __init__(self):
    super(sample, self).__init__()
    self.main = nn.ConvTranspose2d(1, 3, 4, 1, 0, bias = False)

  def forward(self, input):
    return self.main(input)

 

다음으로는 우리가 오늘 살펴보려고 하는 메인인 nn.ConvTranspose2d를 가지고 있는 모델을 하나 만들어줍니다.

 

위에 주석에 달려있듯이, 첫 번째 파라미터는 input의 채널수이고 두 번째 파라미터는 output의 채널 수이며

 

세 번째 파라미터는 kernel size, 네 번째 파라미터는 stride, 다섯 번째 파라미터는 padding입니다.

 

계산을 조금 더 간단하게 하기 위해 bias= False로 놓고 실험해보겠습니다.

 

별도의 신경망 연산이 필요 없으므로, 매우 간단하게 layer 딱 1개만 있는 모델을 만들었습니다.

 

 

 

Model = sample()

# Print model's original parameters.
for name, param in Model.state_dict().items():
  print("name: ", name)
  print("Param: ", param)
  print("Param shape: ", param.shape)

 

만들어진 모델의 기본 weight를 살펴보겠습니다.

 

pytorch가 기본적으로 지원하는 초기화 방법에 따라, weight 값이 결정된 것을 확인할 수 있습니다.

 

그리고 shape는 [1, 3, 4, 4]를 가지게 됩니다. 

 

이는 kernel size를 4로 지정했기 때문에, filter의 사이즈가 4x4 형태가 되어서 그렇고 output의 channel을 3으로 지정하였으므로 결괏값이 3개의 채널로 나와야 해서 3이 생겼다고 보시면 되겠습니다.

 

쉽게 해당 convolution 필터를 가로가 4, 세로가 3, 높이가 4인 직육면체로 생각하시면 편합니다.

 

그림으로 표현해보자면 다음과 같은 것이죠.

 

다음으로는 결과를 조금 더 쉽게 보기 위해서, model의 파라미터를 manually 하게 바꿔보겠습니다.

 

지금은 weight가 소수점으로 되어 있고 매우 복잡하기 때문에, 결과를 직관적으로 이해해볼 수 있기 위한 작업입니다.

 

# I makes 48 values from 0.1 to 4.8 and make (1, 3, 4, 4) shape
np_sam = np.linspace(0.1, 4.8, num = 48)
np_sam_torch = torch.Tensor(np_sam)
sam_tor = np_sam_torch.view(1, 3, 4, 4)


# Modify model's parameters using 4 for loops.
with torch.no_grad():
    batch, channel, width, height = Model.main.weight.shape
    
    for b in range(batch):
        for c in range(channel):
            for w in range(width):
                for h in range(height):
                    Model.main.weight[b][c][w][h] = sam_tor[b][c][w][h]

# Check parameter modification.
print("Model weight: ", Model.main.weight)

 

먼저, np_sam이라는 변수로 np.linspace를 이용해 0.1부터 4.8까지 48개의 값을 만들어줍니다. 이렇게 하면 0.1, 0.2, 0.3 .... 4.8까지 0.1을 간격으로 하는 숫자 48개를 만들 수 있습니다.

 

그리고 이를 torch.Tensor로 바꿔주고, shape를 맞추기 위해 (1, 3, 4, 4)의 형태로 맞춰줍니다.

 

다음으로는 model의 파라미터를 수동으로 바꿔주는 코드입니다.

 

먼저 batch, channel, width, height라는 변수로 model의 weight의 shape를 받습니다.

 

그리고 이를 4중 for문을 이용해서 바꿔줍니다.

 

Model.main.weight[b][c][w][h]의 값을 해당하는 인덱스와 동일한 sam_tor의 위치의 값으로 바꾸는 것이죠.

 

물론 일반적으로 computational cost 때문에 4중 for문을 사용하는 것은 비효율적이긴 하지만, 어쨌든 이번 상황에서는 각 차원별로 값이 작기 때문에 연산 자체가 오래 걸리진 않아 이렇게 코드를 짜보았습니다.

 

그리고 마지막으로는 model의 weight들을 print 해봅니다. 실제로 우리가 원하는 대로 잘 바뀌었는지 봐야겠죠.

 

역시나 원하는 대로 잘 바뀌었네요. 

 

이렇게 값들이 simple해야 우리가 결과를 찍었을 때 해석도 쉬울 것입니다.

 

이것도 아까와 마찬가지로 shape가 (1, 3, 4, 4)가 될 텐데요, 이 또한 직육면체 형태로 이해해 볼 수 있습니다.

 

모든 원소의 값이 다 보이시도록 최대한 길게 그렸는데요. 이처럼 가로가 4, 세로가 3, 높이가 4를 만족하는 직육면체를 생각하시면 됩니다.

 

 

result = Model(test_input)

print("Result shape: ", result.shape)
print("Result: ", result)

다음으로는, 아까 만들어뒀던 (1, 1, 2, 3) 짜리 input을 위에서 보여드린 weight을 이용해서 nn.ConvTranspose2d를 사용해 연산한 결과를 출력합니다.

 

결과가 어떻게 나오는지 보겠습니다.

 

 

뭔가 어떤 결과가 나왔는데, 이것만 봐서는 아직 어떤 식으로 연산이 이루어지는지 알 수 없겠죠?

 

그래서 제가 하나하나 한번 뜯어서 분석해보았습니다.

 

 

 

실제 구동 방법 분석

 

 

이해를 조금 쉽게 하기 위해서, 파라미터와 결과 모두 channel을 1차원으로 가정하고 살펴보겠습니다.

 

먼저, 우리의 input은 다음과 같은 2 x 3 형태의 tensor가 됩니다.

 

이를 우리가 설정한 parameter를 이용해서 단순히 곱해줍니다.

 

따라서 다음과 같은 결과를 볼 수 있습니다.

 

 

다음으로는 다른 input 요소들도 어떻게 계산되는지 보겠습니다.

 

 

 

 

 

 

 

자 그러면 input 1부터 6까지 연산을 완료했다면, 결과가 총 6개가 나오게 되겠죠?

 

나온 6개의 결과를 element-wise addition을 통해서 최종 결과를 뽑아냅니다.

 

 

이것이 아까 프로그램을 돌려서 나온 결과와 일치함을 확인할 수 있습니다.

 

 

 

 

따라서, nn.ConvTranspose2d의 연산 과정을 다음과 같이 정리해볼 수 있습니다.

 

 

1. nn.ConvTranspose2d를 만들 때 input값의 channel 수와 설정한 output channel 수, kernel size를 고려하여 weight를 만들어낸다.

 

우리의 경우에는 output channel을 3으로 지정하였고, kernel size를 4로 지정하였으므로 

 

weight의 크기가 (1, 3, 4, 4)가 됩니다. 

 

만약에 input의 channel 수가 3이고, output channel 수가 7이며 kernel size가 4였다면

 

weight의 크기가 (3, 7, 4, 4)가 될 것입니다.

 

즉, weight의 크기는 (input channel, output channel, kernel size, kernel size)가 됩니다.

 

 

 

2. input의 각 element 별로 weight와 곱해줍니다. 만약 stride가 1보다 크다면, 그 값만큼 이동하면서 만들어줍니다.

 

위에서의 예시는 stride 1인 경우를 봤었죠. input의 각 element 별로 weight와 곱해준 결과를 stride 1로 이동하면서 만들어냈습니다.

 

 

 

 

3. 나온 모든 결괏값을 element-wise 하게 더해서 최종 결과를 냅니다.

 

stride가 kernel size보다 작게 되면 서로 겹치는 부분들이 존재합니다. 그럴 때는 단순히 element-wise하게 더해서 결과를 내주면 됩니다.

 

 

 

마지막으로, batch size가 1보다 클 때의 결과도 한번 정리해보면서 마무리 짓겠습니다.

 

만약 input이 (4, 3, 4, 4)의 사이즈를 가지고 있고, 모델이 nn.ConvTranspose2d(3, 7, 4, 1, 0, biase = False)인 layer를 가지고 있다고 가정한다면 어떻게 될까요?

 

먼저 parameter의 값들부터 확인해보겠습니다.

 

input channel이 3이고, output channel이 7이며, kernel size는 4이고, stride는 1이며 padding은 0인 경우입니다.

 

 

그렇다면 weight는 shape가 어떻게 될까요?

 

 

위에서 얘기한 내용을 적용해보면, (3, 7, 4, 4)가 됨을 알 수 있습니다.

 

 

마지막으로 output의 결과는 어떻게 될까요?

 

 

batch size는 그대로 4가 될 것이고, output channel을 7로 정했으니 channel 수는 7이 되겠죠?

 

 

그리고 stride = 1인 상황이고 padding = 0이니까, input 각각에 대해서 kernel size 4 만큼 확장이 되므로

 

 

이것이 1칸씩 이동하는데 input 사이즈가 4개니까, 총 7이 됩니다. (input이 1개면 4, 2개면 5, 3개면 6, 4개면 7이 되는 것이죠. stride = 1이므로 하나씩 이동한다고 보시면 될 것 같아요.)

 

따라서 output의 결과는 (4, 7, 7, 7)이 됩니다.

 

 

 

지금까지 nn.ConvTranspose2d가 어떻게 작동하는지에 대해서 세세하게 알아봤습니다.

 

예시에서는 조금 더 와닿게 만들기 위해서 batch size가 1일 때를 가정하고 설명하였으나,

 

사실상 batch size는 input와 output에서 변동되는 요소가 아니므로 크게 신경 쓰지 않아도 되는 부분이라고 생각합니다.

 

 

혹시라도 보시면서 더 궁금하신 점이나 이해가 안 되시는 점이 있다면 댓글로 추가적으로 설명을 드릴 수 있을 것 같습니다.

 

 

이번 포스팅은 여기서 마치겠습니다.

 

 

 

 

 

 

오늘은 Generative Adversarial networks에 Convolutional neural network를 섞은 모델인 DCGAN에 대해서 리뷰를 해보겠습니다.

 

original paper : arxiv.org/abs/1511.06434

 

Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks

In recent years, supervised learning with convolutional networks (CNNs) has seen huge adoption in computer vision applications. Comparatively, unsupervised learning with CNNs has received less attention. In this work we hope to help bridge the gap between

arxiv.org

 

사실 GAN에 CNN을 섞은 것 자체에 대한 내용은 알고 있었는데, 논문을 읽으면서 단순히 그냥 CNN을 섞은 것이 끝이 아니라 GAN이 학습한 representation에 대해서 탐구해봤다는 점에서도 의미가 있는 논문이라고 생각했습니다.

 

 

시작해보겠습니다.

 

 

Abstract

 

 

최근에, CNN을 활용한 지도 학습은 computer vision applications에서 많이 채택되고 있다. 하지만, 비교적 CNN을 활용한 비지도 학습은 적은 관심을 받고 있다.

 

본 연구에서 우리는 지도 학습에 대한 CNN의 성공과 비지도 학습 사이의 격차를 해소하는데 도움이 되길 바란다.

 

우리는 아키텍처의 제약을 가지고 있는 deep convolutional generative adversarial networks (DCGANs)라고 불리는 CNN의 일종을 도입하며, 이것이 비지도 학습을 위한 강력한 후보 모델임을 검증했다.

 

다양한 이미지 데이터셋에 학습해서, 우리의 deep convolutional adversarial pair가 generator와 discriminator 모두에서 object 부분부터 장면까지 representation의 계층을 학습한다는 확실한 증거를 보여준다.

 

추가적으로, 새로운 task를 위해 학습된 feature를 사용하였으며, 이를 통해 general image representation으로써의 이들의 적용 가능성을 검증하였다.

 

 

1. Introduction

 

 

많은 양의 unlabeled dataset으로부터 재사용할 수 있는 feature representation을 학습하는 것은 활발하게 연구가 이뤄지고 있는 분야이다.

 

Computer Vision의 맥락에서, 이는 good intermediate representation을 학습하기 위해서 사실상 무제한의 unlabeled image와 video를 활용할 수 있게 해 주며, good intermediate representation은 image classification과 같은 다양한 지도 학습 task에 사용될 수 있다.

 

우리는 GAN을 학습시키는 것을 통해 good image representation을 만드는 방법을 제안하며, 후에 지도 학습 task를 위한 feature extractor로써 generator network와 discriminator network의 일부분을 재사용할 수 있다.

 

GANs은 maximum likelihood techniques에 대한 매력적인 대안을 제공한다.

 

추가적으로 그들의 학습 과정과 heuristic cost function (pixel-wise independent mean-square error와 같은)이 없다는 점에서 representation learning에 매력적이라고 주장할 수 있다.

 

GANs는 학습하는 것이 불안정하다고 알려져 있으며 종종 generator에서 터무니없는 output을 생산해내기도 한다.

 

GNA이 학습하는 것, 그리고 multi-layer GANS의 중간 representation에 대해서 이해하고 시각화하려는 시도에 대해서는 매우 제한적인 연구만 존재한다.

 

본 연구에서는 다음과 같은 contribution이 있다.

 

  • 대부분의 환경에서 학습이 안정적이게 만들어주는 Convolutional GANs의 architectural topology에 대한 일련의 제약을 제안하고, 평가한다. (아키텍처를 만들 때 어떤 걸 해도 되는지, 안되는지에 대해서 앞으로 나오게 되는데 이를 의미합니다.)
  • GANs에 의해서 학습된 filter를 시각화하고 특정 필터가 특정 object를 그리는 방법을 학습했음을 경험적으로 보여준다.
  • 우리는 generator가 생성된 샘플의 많은 의미적 특징을 쉽게 조작할 수 있는 흥미로운 벡터 연산 특성을 가지고 있음을 보여준다.

 

2. Related work

 

 

2.1 Representation learning from unlabeled data

 

 

unsupervised representation learning은 이미지의 맥락에서, 그리고 일반적인 컴퓨터 비전 연구에서 잘 연구된 문제이다.

 

Unsupervised representation learning에 대한 전통적인 접근법은 데이터에 대해서 clustering을 하고(예를 들면, K-means와 같은 방법을 사용해서), classification score를 향상하기 위해 이 cluster를 사용하는 것이다. 

 

이미지의 맥락에서, 강력한 image representation을 학습하기 위해서 image patch들의 계층적 클러스터링을 할 수 있다.

 

또 다른 일반적인 방법은 코드의 구성 요소인 이미지를 compact 코드로 인코딩하는 래더 구조를 분리하여 autoencoder를 학습시키는 것이며, 가능한 정확히 이미지를 복원하기 위해서 코드를 decode 한다. (사실 이걸 읽고 명확하게 이해는 안 되지만, 선행 연구 쪽이라서 그냥 넘어갔습니다..)

 

이러한 방법들은 또한 이미지 픽셀로부터 good feature representation을 학습할 수 있다고 알려져 있다.

 

Deep belief networks는 또한 계층적 표현을 학습하는데 있어서 잘 작동한다고 알려져 왔다.

 

 

 

2.2 Generating natural images

 

 

 

이미지 생성 모델은 잘 연구되어 왔으며 parametric과 non-parametric의 두 가지 범주로 나뉜다.

 

먼저, non-parametric model은 종종 존재하는 이미지 혹은 이미지의 패치를 데이터베이스로부터 matching을 하며

 

texture synthesis, super-resolution, in-painting과 같은 분야에서 사용되어 왔다.

 

 

이미지를 생성하기 위한 parametric model은 MNIST나 texture synthesis를 위해 광범위하게 연구되어 왔다.

 

하지만, 현실 세계의 자연스러운 이미지를 만들어내는 것은 최근까지 큰 성공을 거두지 못했다.

 

이미지를 생성하기 위한 variational sampling approach는 어느 정도의 성공을 거두었으나, 샘플들이 종종 흐릿해지는 현상이 나타난다. (VAE를 생각하시면 됩니다..! VAE에 대한 설명과 코드는 이전에 다루었기 때문에, 궁금하신 분들은 찾아보시면 될 것 같아요!)

 

다른 접근법은 iterative forward diffusion process를 사용하여 이미지를 만들어낸다.

 

Generative Adversarial Networks가 생성한 이미지도 noisy 하고 이해할 수 없는 현상이 나타난다.

 

parametric 접근법에 대한 laplacian pyramid extension은 더 높은 퀄리티의 이미지를 보여주지만, 여전히 여러 모델의 chaining에 의해 유입된 noise에 의해서 물체가 흔들리게 보이는 현상이 나타난다.

 

Recurrent network approach와 deconvolution network approach는 최근에 자연스러운 이미지를 만드는 것에 있어서 어느 정도의 성공을 거두었으나, 그들은 supervised tasks를 위해 generator를 활용하지 않았다. (deconvolution network approach에서 generator를 사용하지 않았기 때문에 해당 연구와의 차별점?으로 적은 게 아닐까 싶네요.)

 

 

 

2.3 Visualizing the internal of CNNs

 

 

신경망을 사용하는 것에 있어서 끊임없는 비평 중 하나는 신경망이 black-box 방법이라는 것이며 인간이 사용할 수 있는 간단한 알고리즘의 형태로 네트워크가 무엇을 하는지에 대해 거의 이해하지 못하고 있다는 것이다.

 

CNN의 맥락에서, Zeiler et. al.은 deconvolution과 filtering the maximal activation을 사용하여 네트워크에서 각 convolution filer의 대략적인 목적을 확인할 수 있었다.

 

마찬가지로, 입력에 gradient descent를 사용하여 필터의 특정한 subset을 활성화하는 이상적인 이미지를 확인할 수 있다.

 

 

 

3. Approach and model architecture

 

 

CNN을 사용해서 이미지를 모델링하기 위해 GANs를 확장하려는 과거의 시도는 성공적이지 못했다.

 

이는 LAPGAN의 저자들이 보다 신뢰성 있게 모델링할 수 있는 저해상도 생성 이미지를 반복적으로 상향 조정하는 alternative approach를 개발하도록 동기 부여했다.

 

또한 우리는 supervised literature에서 흔하게 사용되는 CNN 아키텍처를 사용해 GAN을 스케일링하려는 시도에서 어려움들을 마주했다.

 

하지만, 광범위한 모델 탐구 후에 우리는 많은 데이터셋에 대해서 안정적인 학습을 가져오는 아키텍처의 family를 확인하였으며 이는 더 높은 해상도와 더 깊은 생성 모델이 가능하도록 만들었다. (family를 어떻게 번역해야 할지 애매해서 그냥 뒀습니다. 사실상 다양한 모델 아키텍처를 시도해보면서 가장 적합한 형태의 모델 아키텍처를 찾았다는 의미인 것 같네요.)

 

우리 접근법의 핵심은 CNN 아키텍처에 대한 3개의 최근 검증된 변화를 채택하고 수정하는 것이다.

 

 

첫 번째는 max-pooling과 같은 deterministic spatial pooling functions를 stride convolution으로 대체하는 모든 convolutional net은 네트워크가 이것 자체의 spatial downsampling을 학습하도록 허용한다는 것이다.

 

우리는 이 접근법을 generator에서 사용하였으며, 이는 우리의 spatial upsampling을 학습하도록 만들어줬고 discriminator에서도 사용한다.

 

(max pooling은 deterministic 하게, 즉 결정론적으로 공간적인 풀링을 진행하지만, convolution에 stride를 하도록 만들면 자연스럽게 공간적인 정보를 down sampling 하도록 학습을 할 수 있기 때문에 이렇게 하겠다는 의미입니다. 뒤에서 나오겠지만, DCGAN은 max pooling을 사용하지 않고 stride를 통해서 해상도를 줄이는 제약을 가지고 있습니다.)

 

 

두 번째는 마지막 convolutional features에서 fully connected layer를 제거하는 쪽으로의 트렌드이다. 

 

이것에 대한 가장 강력한 예시는 State of The Art (성능이 가장 좋다는 말) 이미지 분류 모델에서 활용되어 온 global average pooling이다. 

 

우리는 global average pooling이 모델의 안정성을 증가시키지만 수렴 속도를 해친다는 사실을 알게 되었다.

 

가장 높은 convolutional feature를 generator와 discriminator의 input와 output 각각에 직접적으로 연결하는 타협안은 잘 작동했다. (직접적으로 연결한다는 의미가, pooling을 쓰지 않고 convolution 연산 만으로 input부터 output까지 연결한다는 의미인 것 같습니다.)

 

Uniform noise distribution $Z$를 input으로 받는 GAN의 첫 번째 layer는 단순히 행렬 곱이기 때문에 fully connected라고 불릴 수 있지만, 결과는 4차원 tensor로 reshape 되고 convolution stack의 출발지점으로 사용된다. 

 

(해당 내용은 모델 아키텍처에서 이 부분을 이야기하는 것으로 보입니다.)

 

Discriminator에서는, 마지막 conv layer는 flatten 되고 하나의 sigmoid output으로 들어간다. 모델 아키텍처의 한 예시를 시각화한 것이 Fig. 1에 나와있다.

 

 

Figure 1

 

세 번째는 각 unit에 들어가는 input이 평균이 0이고 분산이 1이 되도록 normalize 해주는 것을 통해 학습을 안정적으로 만들어주는 Batch Normalization이다. 

 

이는 안 좋은 초기화 때문에 발생하는 학습 문제를 해결하고 더 깊은 모델에서 gradient 흐름을 도와준다.

 

이는 deep generator가 학습을 시작할 때 모든 샘플들이 GANs에서 발견되는 일반적인 failure mode인 하나의 point로 collapse 되는 것을 방지한다. (GAN의 학습 초기에 같은 모양만 생성하는 현상을 이야기하는 것 같습니다.)

 

하지만, 모든 layer에 직접적으로 batch norm을 적용하는 것은 sample oscillation과 모델 불안정성을 야기한다.

 

이는 generator의 output layer와 discriminator의 input layer에 batchnorm을 적용하지 않는 것을 통해 피할 수 있다.

 

 

 

ReLU activation은 Tanh function을 사용하는 output layer만 빼고 generator의 모든 layer에서 사용된다.

 

우리는 bounded activation을 사용하는 것이 모델로 하여금 더 빠르게 saturate 하도록 학습시키고 학습 분포의 color space를 포괄하도록 학습함을 발견했다. 

 

우리는 Discriminator 내에서 leaky rectified activation이 잘 작동함을 확인하였고, 특히 높은 resolution modeling에서 그러하다. (leaky rectified activation은 보통 LeakyReLU라고 부르는 활성화 함수인데, ReLU는 $x < 0$ 구간에서는 0이고 $x >=0$에서는 $x$의 값을 가지지만, LeakyReLU는 $x < 0$ 구간에서는 $ax$이고, $x >= 0$ 구간에서는 $x$입니다.)

 

이는 maxout activation을 사용한 original GAN paper와는 대조적이다. 

 

 

안정적인 DCGAN을 위한 아키텍처 가이드라인

 

  • Pooling layer를 strided convolution (discriminator)와 fractional-strided convolution (generator)로 교체한다.
  • Generator와 discriminator 모두에서 Batch Norm을 사용한다.
  • 더 깊은 아키텍처를 위해 fully connected hidden layer를 제거한다.
  • Generator에서 Tanh를 사용하는 output layer를 제외한 모든 layer에서 ReLU activation을 사용한다.
  • Discriminator의 모든 layer에서 LeakyReLU activation을 사용한다.

 

 

 

4. Details of adversarial training

 

 

우리는 DCGANs를 Large-scale Scene Understanding (LSUN), Imagenet-1k, 새롭게 모은 Face dataset의 총 3개의 데이터셋에 학습했다. 각 데이터셋의 사용에 대한 세부사항은 아래에 나와있다.

 

학습 이미지에 대해서는 tanh activation function의 범위인 [-1, 1]에 맞추기 위한 스케일링을 제외하고는 어떤 전처리도 적용되지 않았다. 

 

모든 모델은 mini-batch stochastic gradient descent (SGD)를 이용해서 학습되었으며, 128 사이즈의 mini-batch를 사용했다.

 

모든 weight는 standard deviation 0.02를 가지는 zero-centered normal distribution을 이용하여 초기화되었다.

 

LeakyReLU에서는, leak의 slope는 모든 모델에서 0.2로 고정한다.

 

이전 GAN 연구들은 학습을 가속화하기 위해서 momentum을 사용하였으나, 우리는 튜닝된 hyperparameters를 가지고 Adam optimizer를 사용했다.

 

우리는 0.001의 learning rate가 너무 높다고 판단해 0.0002를 사용했다.

 

추가적으로, momentum term $\beta_1$을 제안된 값 0.9가 training oscillation과 불안정을 야기하여 학습을 안정화시키고자 0.5로 줄였다.

 

 

 

4.1부터 4.3 까지는 개별 데이터셋의 적용 내용이므로, 생략합니다.

 

 

 

5. Empirical validation of DCGANs capabilities

 

5.1 Classifying CIFAR-10 using GANs as a feature extractor

 

 

unsupervised representation learning algorithm의 품질을 평가하는 하나의 방법은 supervised dataset에 이를 feature extractor로 적용해보는 것이며 이 feature의 top에 linear model을 fitting 시켜서 성능을 평가하는 것이다.

 

CIFAR-10 dataset에 대해서, feature learning algorithm으로 K-means를 활용하는 single layer feature extraction pipeline으로부터 매우 강력한 baseline performance가 검증되어 왔다.

 

매우 많은 양의 feature maps (4800)을 사용했을 때, 이 방법은 80.6% accuracy를 달성했다.

 

이 방법을 unsupervised multi-layered로 확장하면 82.0% accuracy를 달성했다.

 

Supervised tasks를 위해 DCGANs에 의해서 학습된 representation의 품질을 평가하기 위해 DCGAN을 Imagenet-1k에 학습하고 discriminator의 모든 layer에서 나온 convolutional feature를 사용하였으며 4x4 spatial grid를 만들기 위해서 각 layer의 representation을 max-pooling 했다.

 

이 feature들을 flatten 하고 이를 concatenate 해서 28672차원의 벡터를 만든 다음 이에 대해서 regularized linear L2-SVM classifier를 학습시킨다.

 

이는 82.8% accuracy를 달성했으며, 모든 K-means 기반 접근법보다 더 좋은 성능을 달성했다.

 

특히 discriminator는 K-means 기반의 기법과 비교했을 때 훨씬 적은 feature map을 가지지만 (가장 높은 layer에서 512개) 4x4 spatial locations의 많은 layer 때문에 더 큰 전체 feature vector size를 갖게 된다.

 

추가적으로, 우리의 DCGAN은 CIFAR-10에 학습이 되지 않았으며, 이 실험은 또한 학습된 feature의 domain robustness를 증명했다. 

 

Table 1

(Table 1에서는 기존의 K-means와 같은 unsupervised 방법론에 비해서 훨씬 더 좋은 accuracy를 보임을 나타내고 있습니다. 이를 통해, DCGAN이 학습한 unsupervised representation의 품질이 좋음을 알 수 있습니다.)

 

 

5.2는 SVHN 데이터에 실행한 결과라서, 생략하겠습니다.

 

 

6. Investigating and visualizing the internals of the networks

 

 

우리는 학습된 generator와 discriminator를 다양한 방식으로 살펴볼 것이다. 우리는 training set에 대해서 어떠한 종류의 nearest neighbor search를 하지 않았다.

 

pixel이나 feature space에서의 nearest neighbor는 작은 이미지 변환에 의해서 하찮게 속게 된다.

 

우리는 또한 모델을 정량적으로 평가하기 위해서 log-likelihood metrics를 사용하지 않았으며, 이는 안 좋은 metric이기 때문이다.

 

 

6.1. Walking in the latent space

 

 

우리가 한 첫 번째 실험은 latent space의 landscape를 이해하는 것이다. 학습된 manifold에서 걷는 것은 일반적으로 우리에게 memorization의 signs에 대해서 말해줄 수 있고 (만약 sharp transitions이 있는 경우) 공간이 계층적으로 collapsed 된 방법에 대해서도 말해줄 수 있다.

(latent space에서 값을 점차적으로 변화함에 따라서, 이미지가 확 바뀌게 되면 이는 training sample을 학습했다기보다는 그저 기억한다고 볼 수 있습니다. 그 부분을 memorization의 sign이라고 표현한 것으로 보입니다. )

 

만약 latent space에서 걷는 것이 이미지 생성에 대해서 의미론적인 변화를 야기하는 경우(객체가 추가되거나 제거되는 것을 의미) 우리는 모델이 관련되어 있는 표현을 학습했고 흥미로운 표현을 학습했다고 추론할 수 있다. 

 

결과는 Fig. 4에서 확인할 수 있다.

 

 

Figure 4

 

 

6.2 Visualizing the discriminator features

 

 

 

이전 연구는 큰 이미지 데이터셋에 CNN을 supervised training 했을 때 매우 강력한 학습된 feature를 야기한다는 사실을 보였다.

 

추가적으로, scene classification에 학습된 supervised CNN은 object detectors를 학습한다.

 

우리는 large image dataset에 학습된 unsupervised DCGAN도 역시 흥미로운 특징의 계층을 학습할 수 있음을 보인다.

 

Springenberg et al. 에 의해 제안된 guided backpropagation을 사용해서, Fig.5에서 discriminator에 의해서 학습된 feature가 침대나 창문과 같은 bedroom의 특정한 부분에서 활성화된다는 것을 보였다.

 

비교를 위해서, 같은 이미지에서, 우리는 의미론적으로 관련이 있거나 흥미로운 어떤 것에 활성화되지 않은 임의로 초기화된 feature에 대한 baseline을 제공한다.

 

(guided backpropagation을 정확히 해당 논문에서 설명하지 않고 있어서, 저도 정확히는 알 수 없지만 discriminator가 진짜라고 판별하는 데 있어서 가장 크게 기여한 부분을 표현한 이미지가 아닐까 라는 생각이 듭니다.)

 

Figure 5

 

 

6.3 Manipulating the generator representation

 

 

6.3.1 forgetting to draw certain objects

 

 

Discriminator에 의해서 학습된 표현에 더하여, generator가 학습한 표현이 무엇인지에 대한 질문이 있다.

 

샘플의 품질은 generator가 베개, 창문, 램프, 문, 그리고 이것저것 다양한 가구와 같은 주요한 장면 요소에 대한 구체적인 object representation을 학습했음을 시사한다.

 

이러한 표현들이 취하는 형태를 탐구하기 위해, 우리는 generator에서 창문을 완전히 제거하려고 실험을 수행했다.

 

 

150개 sample에 대해서, 52개의 창문 bounding box를 직접 그렸다.

 

두 번째로 높은 convolution layer feature에 대해서, logistic regression은 bounding box 안쪽에 있는 activation은 positive로, 같은 이미지에서 랜덤한 샘플을 뽑아서 negative로 지정하는 기준을 사용해 feature activation이 window에 있는지 없는지를 예측하기 위해 fitting 되었다.

 

이 간단한 모델을 사용하면, 0보다 큰 weight를 가지는 모든 feature map이(총 200개) 모든 spatial location으로부터 떨어져 나간다. 

 

그러고 나서, 임의의 새로운 샘플은 feature map removal을 가지고 만들어지고, 없이 만들어진다.

 

(솔직히 이 내용만 봐서는 정확히 어떻게 만들었는지가 와 닿지는 않은데, 특히 weight가 0보다 큰 경우에 해당 activation을 어떻게 처리하는지? 이 부분이 살짝 모호한 느낌이네요.)

 

 

Window dropout을 가지고 만들어진 이미지와 없이 만들어진 이미지는 Fig. 6에 나와있다.

 

그리고 흥미롭게도, 네트워크는 대개 bedroom에 window를 그리는 것을 까먹고, 이를 다른 object로 대체했다.

 

Figure 6

 

6.3.2 Vector arithmetic on face samples

 

 

단어의 학습된 학습된 representation을 평가하는 맥락에서 (Mikolov etal., 2013) 단순한 산술 연산이 representation space에서 풍부한 linear structure를 나타낸다는 것이 검증되었다.

 

하나의 표준이 되는 예시는 vector("King") - vector("Man") + vector("Woman")이 Queen을 의미하는 vector에 가장 가까운 vector를 야기한다는 사실을 나타낸다.

 

우리는 유사한 구조가 우리의 generator의 $Z$ representation에서 나타나는지 아닌지를 살펴보았다.

 

우리는 시각적 개념에 대한 예시 샘플 세트의 $Z$ 벡터에 대해 유사한 산수를 수행했다.

 

개념 당 오직 하나의 샘플로 실험을 진행하는 것은 불안정했으나, 3개의 예시에 대한 $Z$ vector를 평균 내는 것은 의미론적으로 산수를 따르는 안정적이고 일관된 생성을 보여주었다.

 

Fig. 7에 나타난 object manipulation에 더해서, 우리는 face pose 또한 $Z$ space에서 선형적으로 모델링 될 수 있음을 검증하였다. (Fig. 8)

 

Figure 7
Figure 8

 

 

이러한 검증은 우리 모델에 의해서 학습된 $Z$ representation을 사용하여 흥미로운 application이 개발될 수 있음을 제안한다.

 

Conditional generative model이 scale, rotation, position과 같은 object attribute를 설득력 있게 모델링하는 방법을 학습할 수 있음은 이전에 증명되었다. (Dosovitskiy et al., 2014)

 

이는 우리가 알기로는 완전히 unsupervised models에서 이러한 현상이 발생한다는 것을 증명한 첫 번째 사례이다.

 

위에서 언급된 벡터 산수를 더 탐구하고 개발하는 것은 복잡한 이미지 분포의 conditional generative modeling에 필요한 데이터의 양을 극적으로 줄일 수 있다.

 

 

7. Conclusion and future work

 

 

우리는 generative adversarial networks를 학습시키는 더욱 안정적인 아키텍처 set을 제안하였으며 adversarial networks가 generative modeling과 supervised learning을 위한 good image representation을 학습한다는 것에 대한 증거를 제시했다.

 

여전히 모델 불안정성의 몇 가지 형태가 남아 있으며 우리는 모델이 더 오래 학습할수록 그들이 때때로 filter의 subset을 하나의 oscillating mode로 붕괴된다는 것을 알고 있다.

 

이후 연구는 이런 불안정성의 형태를 다룰 필요가 있다.

 

우리는 이 framework를 video (frame prediction을 위한)나 audio (speech synthesis를 위한 pre-trained feature)와 같은 다른 분야로 확장하는 것이 매우 흥미로울 것이라고 생각한다.

 

학습된 latent space에 대한 특성을 더욱 조사해보는 것은 또한 흥미로울 것이다.

 

 

 

여기까지 DCGAN 논문에 대한 내용들을 쭉 다뤄보았는데요.

 

이전에 GAN을 이용해서 MNIST를 만들어본 적이 있지만, 아무래도 엄청 간단한 모델이고 하다보니 숫자처럼 보이는 샘플들이 나오기는 했으나 그렇게까지 좋은 샘플이 나오지는 못했습니다.

 

아마도 DCGAN을 활용하면 조금 더 진짜 같은 이미지가 나오지 않을까? 하는 기대를 해보면서 다음 글에서는 code review로 찾아오겠습니다.

 

 

 

이번 글에서는 지난 글에서 review한 AAE 논문을 코드로 구현한 내용을 살펴보겠습니다.

 

 

AAE 논문의 paper review : cumulu-s.tistory.com/26

 

3. Adversarial Autoencoders(AAE) - paper review

오늘은 Autoencoder의 구조와 GAN framework를 결합해 탄생한 AAE에 대해서 알아보도록 하겠습니다. paper : arxiv.org/abs/1511.05644 Adversarial Autoencoders In this paper, we propose the "adversarial aut..

cumulu-s.tistory.com

 

AAE 논문에서는, AAE를 활용할 수 있는 다양한 applications을 제시하였는데 일단 저는 가장 기초적인 AAE를 짜봤습니다.

 

 

hyperparameter도 바꿔보면서, 수십번의 시도를 하였음에도 논문에서 나온대로 Encoder가 manifold learning을 하지 못해서 코드가 완벽하다고 하기는 어려울 것 같습니다. (GAN에서 discriminator와 generator가 적절한 수준에서 학습시키기가 어려운 것 때문으로 추측하고 있습니다. 이럴땐 공식 코드가 있고 없고의 차이가 크다고 느껴지네요 ㅠㅠ)

 

 

대략 이런식으로 코드를 짜보면 되겠구나 정도로 참고해주시면 되겠습니다.

 

 

import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision.datasets as dsets
import torchvision.transforms as transforms
from torch.autograd import Variable
from torch.utils.tensorboard import SummaryWriter
import datetime
import os
import torchvision
import itertools
import numpy as np
from math import sin,cos,sqrt

먼저 필요한 package들을 import 해줍니다.

 

 

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/AAE_Result', current_time)
os.mkdir(saved_loc)

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

writer = SummaryWriter(saved_loc)

 

이 부분은 이전 코드 리뷰에서도 이미 다뤘던 부분이라서, 넘어가도록 하겠습니다.

 

직접 돌리실 때는 경로를 원하시는 위치로 바꿔주시면 됩니다.

 

 

# MNIST Dataset 
dataset = dsets.MNIST(root='/content/drive/MyDrive/MNIST', 
                      train=True, 
                      transform=transforms.ToTensor(),  
                      download=True)

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

testset = dsets.MNIST(root='/content/drive/MyDrive/MNIST', 
                      train=False, 
                      transform=transforms.ToTensor(),  
                      download=True)

testloader = torch.utils.data.DataLoader(dataset=testset,
                                         batch_size = 200,
                                         shuffle = True)

사용할 데이터셋인 MNIST를 불러와줍니다.

 

 

 

def to_var(x):
    if torch.cuda.is_available():
        x = x.cuda()
    return Variable(x) 

이 함수는, x 라는 변수를 GPU 사용이 가능할 때, cuda로 옮겨주고 이를 Variable로 만들어주는 코드입니다.

 

 

#Encoder
class Q_net(nn.Module):  
    def __init__(self,X_dim,N,z_dim):
        super(Q_net, self).__init__()
        self.lin1 = nn.Linear(X_dim, N)
        self.lin2 = nn.Linear(N, N)
        self.lin3gauss = nn.Linear(N, z_dim)

        self.lin1.weight.data.normal_(0, 0.01)
        self.lin2.weight.data.normal_(0, 0.01)
        self.lin3gauss.weight.data.normal_(0, 0.01)
    def forward(self, x):
        x = F.dropout(self.lin1(x), p=0.25, training=self.training)
        x = F.relu(x)
        x = F.dropout(self.lin2(x), p=0.25, training=self.training)
        x = F.relu(x)
        xgauss = self.lin3gauss(x)
        return xgauss

다음으로는 Encoder 부분입니다. X_dim은 이미지의 사이즈가 될 것이고, N은 중간 hidden layer의 node 수 입니다. z_dim는 latent space의 차원이구요.

 

논문의 Appendix A에서 'The weights are initialized with a Gaussian distribution with the standard deviation of 0.01.'라고 적혀 있으므로, weight.data.normal_(0, 0.01)을 이용해서 초기화를 해줍니다.

 

그리고 'The activation of the last lyaer of $q(z|x)$ is linear'라고 적혀 있으므로, 마지막 layer에는 activation function을 따로 만들지 않습니다.

 

# Decoder
class P_net(nn.Module):  
    def __init__(self,X_dim,N,z_dim):
        super(P_net, self).__init__()
        self.lin1 = nn.Linear(z_dim, N)
        self.lin2 = nn.Linear(N, N)
        self.lin3 = nn.Linear(N, X_dim)

        self.lin1.weight.data.normal_(0, 0.01)
        self.lin2.weight.data.normal_(0, 0.01)
        self.lin3.weight.data.normal_(0, 0.01)

    def forward(self, x):
        x = F.dropout(self.lin1(x), p=0.25, training=self.training)
        x = F.relu(x)
        x = F.dropout(self.lin2(x), p=0.25, training=self.training)
        x = F.relu(x)
        x = self.lin3(x)
        return torch.sigmoid(x)

 

다음은 Decoder 입니다. Decoder도 동일하게 weight 초기화를 적용해주고, 이번에는 마지막 layer의 결과가 이미지와 동일하게 0부터 1사이의 값이 나와야하므로 sigmoid를 마지막에 적용해줍니다.

 

 

# Discriminator
class D_net_gauss(nn.Module):  
    def __init__(self,N,z_dim):
        super(D_net_gauss, self).__init__()
        self.lin1 = nn.Linear(z_dim, N)
        self.lin2 = nn.Linear(N, N)
        self.lin3 = nn.Linear(N, 1)

        self.lin1.weight.data.normal_(0, 0.01)
        self.lin2.weight.data.normal_(0, 0.01)
        self.lin3.weight.data.normal_(0, 0.01)
    def forward(self, x):
        x = F.dropout(self.lin1(x), p=0.2, training=self.training)
        x = F.relu(x)
        x = F.dropout(self.lin2(x), p=0.2, training=self.training)
        x = F.relu(x)
        return torch.sigmoid(self.lin3(x))  

 

다음 코드는 Discriminator입니다. latent vector를 입력으로 받아, 해당 latent vector가 진짜인지 가짜인지 판별하는 역할을 하죠. 

 

 

 

EPS = 1e-15
z_red_dims = 2
Q = Q_net(784,1000,z_red_dims).cuda()
P = P_net(784,1000,z_red_dims).cuda()
D_gauss = D_net_gauss(1000,z_red_dims).cuda()

EPS는 매우 작은 숫자로 설정되어 있고, latent dimension은 2로 설정했습니다.

 

그리고 논문의 Appendix A에 'The encoder, decoder and discriminator each have two layers of 1000 hidden units with ReLU activation function'으로 적혀 있으므로, hidden layer의 node는 1000개로 지정하였습니다.

 

 

# Set learning rates
gen_lr = 0.0001
reg_lr = 0.00005

# encode/decode optimizers
optim_P = torch.optim.Adam(P.parameters(), lr=gen_lr)
optim_Q_enc = torch.optim.Adam(Q.parameters(), lr=gen_lr)
# regularizing optimizers
optim_Q_gen = torch.optim.Adam(Q.parameters(), lr=reg_lr) 
optim_D = torch.optim.Adam(D_gauss.parameters(), lr=reg_lr)
    
data_iter = iter(data_loader)
iter_per_epoch = len(data_loader)
total_step = 60000 

Reconstruction error를 최소화하는 상황에서 Encoder와 Decoder는 0.0001의 learning rate를 가지고 학습하도록 설계하였으며, Regularization error를 최소화하는 상황에서 Discriminator와 generator는 0.00005의 learning rate를 가지고 학습하도록 설계하였습니다.

 

data_iter라는 이름으로 data_loader를 iterator로 만들어주고, 전체 training step은 6만번 진행합니다. (batch size가 100개라서, 600번 진행하면 1 epoch이며, 6만번 진행하면 100 epoch이 됩니다.)

 

iter_per_epoch는 600이 나옵니다.

 

 

for step in range(total_step):

    # Reset the data_iter
    if (step+1) % iter_per_epoch == 0:
        data_iter = iter(data_loader)

    # Fetch the images and labels and convert them to variables
    images, labels = next(data_iter)
    images, labels = to_var(images.view(images.size(0), -1)), to_var(labels)

 

다음으로는 반복문 부분을 보겠습니다.

 

앞에서 iterator를 정의했었는데, 만약 600번(1 에폭)이 지나면 새롭게 다시 iterator를 reset 해줍니다.

 

그리고 iterator는 next()라는 명령어를 통해서 한개씩 데이터를 빼낼 수 있기 때문에 next(data_iter)를 적용해줍니다.

 

next를 통해서 빼넨 이미지는 (100, 1, 28, 28)의 형태를 가지고 있는데 앞에서 정의한 Encoder는 (Batch_size, 28*28)의 형태를 input으로 받습니다. 따라서, images.view(images.size(0), -1)를 통해서 (100, 784) 사이즈로 맞춰줍니다.

 

딥러닝에서는 항상 shape가 매우 중요하니까, 코드를 보실 때 이런 부분을 염두에 두시고 보시는게 좋다고 생각합니다.

 

 

    # ======================================== #
    # =====Phase 1 : Reconstruction Loss====== #
    # ======================================== #
    
    P.zero_grad()
    Q.zero_grad()
    D_gauss.zero_grad()

    z_sample = Q(images)   #encode to z
    X_sample = P(z_sample) #decode to X reconstruction
    recon_loss = F.binary_cross_entropy(X_sample+EPS, images+EPS, reduction = 'sum')

    writer.add_scalar("Train/Reconstruction_loss", recon_loss.item() / len(images), step)

    recon_loss.backward()
    optim_P.step()
    optim_Q_enc.step()

다음으로는 Loss를 계산하고, backprop 시켜보겠습니다.

 

먼저, Encoder와 Decoder, Discriminator를 모두 zero_grad() 해줍니다.

 

z_sample은 이미지를 latent vector로 바꾼 것을 저장한 것이고, X_sample은 latent vector를 이용해 이미지를 복원한 결과입니다.

 

그리고 recon_loss는 X_sample과 원래 이미지를 binary_cross_entropy를 이용해서 loss 값을 계산해줍니다.

 

writer.add_scalar는 reconstruction loss 값을 tensorboard에 write하는 코드구요.

 

계산된 recon_loss에 대해서 .backward()를 해주면 loss에 대한 각 변수들의 gradient가 계산됩니다. 

 

그리고 나서 optim.step()을 통해서 optimizer에 지정된 모델의 매개변수들을 계산된 gradient를 가지고 업데이트 해줍니다.

 

 

    # ======================================== #
    # =====Phase 2 : Discriminator Loss======= #
    # ======================================== #

    Q.eval()
    z_real_gauss = Variable(torch.randn(images.size()[0], z_red_dims) * 5.).cuda()
    D_real_gauss = D_gauss(z_real_gauss)

    z_fake_gauss = Q(images)
    D_fake_gauss = D_gauss(z_fake_gauss)

    D_loss = -torch.mean(torch.log(D_real_gauss + EPS) + torch.log(1 - D_fake_gauss + EPS))

    writer.add_scalar("Train/Discriminator_loss", D_loss.item() / len(images), step)

    D_loss.backward()
    optim_D.step()

 

다음 단계는 Discriminator loss를 계산하고, 업데이트 하는 코드입니다.

 

먼저, Q(Encoder)를 추론 모드로 전환해줍니다. (Q.eval()을 하면 해당 모델의 파라미터가 업데이트 되지 않습니다.)

 

z_real_gauss라는 이름으로 평균이 0이고 표준편차가 5인 가우시안 분포에서 임의의 값들을 만들어냅니다.

 

torch.randn(images.size()[0], z_red_dims)를 이용하면 평균이 0이고 표준편차가 1짜리인 (100, 2) shape를 가지는 값들을 만들게 되는데, 여기에 5.를 곱해서 표준편차가 5가 되도록 만든 코드라고 보시면 됩니다.

 

이 가우시안 분포 값을 Discriminator에 넣어서 나온 결과를 D_real_gauss로 저장합니다. 

 

다음으로는 우리가 가지고 있는 training image를 Encoder에 넣어서 나온 결과를 z_fake_gauss로 저장하고, 이를 Discriminator에 넣어서 나온 결과를 D_fake_gauss로 저장합니다. 

 

그리고나서, GAN의 Discriminator loss 계산 방법을 그대로 적용하여 D_loss를 계산해줍니다. 

 

참고로, 여기서 EPS를 각각 더해주는데, 궁금해서 이걸 빼보니까 에러가 나더라고요. 확실하진 않은데 이미지가 모두 0이여 버리면 gradient 연산을 해줄 때 문제가 발생하나봅니다. 그래서 엄청나게 작은 양수를 넣어줘서 그런 연산 이슈를 해결하려고 EPS라는 값을 넣어주는 것 같습니다. 1e-15면 엄청나게 작은 값이라서 실제 연산 결과에는 영향을 미치지 않을 정도죠.

 

이번 코드에서는 Discriminator만 학습을 시켜주면 되기 때문에 D_loss.backward()를 해주고 optim_D.step()을 해줍니다.

 

 

 

    # ======================================== #
    # =======Phase 3 : Generator Loss========= #
    # ======================================== #

    Q.train()

    z_fake_gauss = Q(images)
    D_fake_gauss = D_gauss(z_fake_gauss)
    
    G_loss = - torch.mean(torch.log(D_fake_gauss + EPS))

    G_loss.backward()
    optim_Q_gen.step()

    writer.add_scalar("Train/Generator_loss", G_loss.item() / len(images), step)

마지막으로 Generator loss를 계산해봅니다.

 

이번에는 encoder이자 generator인 Q를 학습해야 하므로, Q.train()으로 학습 모드를 만들어줍니다.

 

그리고 training image를 encoder에 넣어서 latent vector로 만들어주고, 이를 Discriminator에 넣어서 계산해줍니다.

 

GAN에서 얘기한 Generator loss를 동일하게 적용해주면 완성이 됩니다.

 

 

 if (step+1) % 100 == 0:
        print('Step [%d/%d], Recon_Loss: %.4f, D_loss: %.4f, G_loss: %.4f' 
              %(step+1, total_step, recon_loss.item() / len(images), D_loss.item() / len(images), G_loss.item() / len(images)))

학습이 잘 되고 있는지 확인해야 하니까, step 100번마다 reconstruction loss와 discriminator loss, generator loss가 찍히도록 짰습니다.

 

 

 

if (step+1) % 300 == 0:

        # Test the model's ability to reconstruct test set image. 
        
        Q.eval()
        P.eval()

        sample, _ = next(iter(testloader))

        sample = to_var(sample.view(sample.size(0), -1)) # (100, 784)

        z_ = Q(sample) # (100, 120)
        recon_ = P(z_) # (100, 784)

        n = min(sample.size(0), 8)
        comparison = torch.cat([sample.view(100, 1, 28, 28)[:n], recon_.view(100, 1, 28, 28)[:n]])
        grid = torchvision.utils.make_grid(comparison.cpu())
        writer.add_image("Test image - Above: Real data, below: reconstruction data", grid, (step+1)/300 )
        
        # Test the model's ability to decode latent variable to image data
        
        z_test = Variable(torch.randn(16, z_red_dims) * 5.).cuda()
        recon_image = P(z_test).cpu()
        grid = torchvision.utils.make_grid(recon_image.view(16, 1, 28, 28))
        writer.add_image("Latent to Image", grid, (step+1)/300 )

그리고 300번마다 제대로 잘 학습하고 있는지를 파악하기 위해서 다음과 같은 코드를 짰습니다.

 

먼저, Encoder와 Decoder는 inference mode로 바꿔주고, testloader에서 샘플을 받아옵니다.

 

즉, 모델이 아직 본적 없는 이미지들에 대해서 시험을 해보는 것이죠.

 

학습된 Q(Encoder)에 test 이미지를 넣어서 latent vector로 만들어줍니다. 이를 z_로 저장해주고요.

 

그리고 이 z_를 가지고 Decoder에 집어 넣어서 원래 이미지를 복원해줍니다. 이를 recon_으로 저장해줍니다.

 

다음으로는 원래 기존 test image 중에서 8개, 그리고 복원된 이미지 중에서 8개를 가져온 다음 이를 torch.cat을 통해서 concat 해줍니다. sample.view(100, 1, 28, 28)[:n]를 하면 (8, 1, 28, 28)가 될 것이고, recon_.view도 마찬가지로 (8, 1, 28, 28)가 됩니다.

 

여기에 torch.cat을 사용해주면 (16, 1, 28, 28)짜리 이미지가 만들어질 것이고, 이를 torchvision의 make_grid를 통해서 그리드 이미지로 만들어줍니다. 이 때, comparison은 모두 cuda에 올라가 있으므로, cpu()를 통해서 다시 cpu쪽으로 이동시켜줍니다. (GPU를 활용한 cuda 연산을 할 때는 값들이 cuda에 올라가 있는지 cpu에 있는지를 잘 확인해야 합니다.)

 

이를 writer.add_image를 통해서 tensorboard레 써줍니다.

 

그리고 z_test라는 이름으로 평균이 0이고 표준편차가 5짜리로 (16, 2) 차원의 랜덤한 값들을 만들어주고,

 

이를 Decoder에 넣어서 복원해줍니다. recon_image라고 저장해뒀죠.

 

그리고 이를 (16, 1, 28, 28)로 shape를 변경하고, 다시 한번 make_grid를 이용해서 그리드 이미지로 만들어줍니다.

 

이를 tensorboard에 똑같이 저장해주죠.

 

 

해당 코드를 통해서 보고자 하는 것은 다음과 같습니다.

 

1) 모델이 내가 본 적 없는 테스트 이미지를 이미지 => Encoder => Decoder => 복원 과정을 거치면서 제대로 복원 시킬 수 있는 능력을 갖추고 있는가?

 

2) 임의의 $N(0, 5)$를 따르는 값들을 만들어서 Decoder에 투입하였을 때, 적절한 형태의 이미지를 만들어낼 수 있는가? 

 

 

# save the Model
torch.save(Q.state_dict(), os.path.join(saved_loc, 'Q_encoder_weights.pt'))
torch.save(P.state_dict(), os.path.join(saved_loc, 'P_weights.pt'))
torch.save(D_gauss.state_dict(), os.path.join(saved_loc, 'D_weights.pt'))

마지막은 그냥 모델 weight 저장하는 코드니까, 별도의 설명 없이 넘어가겠습니다. 

 

 

여기까지가 코드이고, 다음으로는 결과를 보여드리겠습니다.

 

 

 

Train 과정에서의 Reconstruction error와 Discriminator loss, Generator loss입니다.

 

Reconstruction error는 계속해서 줄어들고 있는 모습을 보이고 있구요.

 

Discriminator loss는 우상향, Generator loss는 우하향 하고 있는 모습을 보이고 있습니다.

 

Generator loss가 $-log(D(G(z)))$ 임을 감안한다면, 이것이 감소한다는 의미는 $log(D(G(z)))$가 증가한다는 의미이고

 

Log 그래프를 생각해보시면, 이것이 증가한다는 의미는 Discriminator가 $G(z)$를 더욱 진짜라고 판단한다 라고 볼 수 있습니다.

 

 

다음으로는, Test를 보겠습니다.

 

step 1200 일 때의 결과 - latent dim : 2

아까 위에서 설명드린대로, 이는 (16, 2) 차원의 Gaussian random vector를 가지고 Decoder를 이용해 복원한 결과입니다.

 

완전 초창기 결과라서 성능이 매우 좋지 않음을 확인할 수 있습니다.

 

 

step 60000 일 때의 결과 - latent dim : 2

 

다음은 100 epoch, 즉 step 60000일 때(학습 완료)의 결과입니다. 아까보다는 조금 더 진짜 같은 이미지들이 나오고 있기는 하지만, 생각보다는 성능이 썩 좋지 않네요.

 

 

다음으로는 복원 성능을 보겠습니다.

 

 

step 1200 일 때의 복원 성능 - latent dim : 2

 

완전 초창기라서 사실상 이미지를 제대로 복원하지 못하고 있음을 확인할 수 있습니다.

 

 

 

step 60000 일 때의 복원 성능 - latent dim : 2

 

 

100 에폭 동안 학습하고 난 결과 입니다. 9가 3처럼 나오는 등 살짝 문제가 있기는 하지만, 그래도 훨씬 더 복원 성능이 좋아졌습니다.

 

 

 

다음으로는, latent dimension을 15로 했을 때의 결과를 보여드리겠습니다.

 

 

 

이전보다 Generator 그래프와 Reconstruction 그래프는 훨씬 깔끔해진 것을 확인할 수 있었습니다.

 

 

반면에, Discriminator의 그래프가 영 심상치 않네요.

 

 

다음으로는, Test도 보겠습니다.

 

 

step 1200 일 때의 결과 - latent dim: 15

 

역시나 학습 초기라 멀쩡하지 못한 이미지가 나오고 있습니다.

 

 

step 60000 일 때의 결과 - latent dim : 15

 

뭔가 글씨의 모습이 썩 좋지 못하게 나왔습니다. 오히려 잠재 공간이 2차원일 때 보다 더욱 안 좋아진 것 같습니다.

 

 

다음으로는 복원 성능도 보겠습니다.

 

 

step 1200 일 때의 결과 - latent dim : 15

 

step 60000 일 때의 결과 - latent dim : 15

 

latent 차원이 15차원일 때, 복원 성능은 엄청나게 좋아진 것을 확인할 수 있습니다.

 

Decoder 성능이 매우 안 좋게 나왔음에도 15차원의 결과를 여기에 담게 된 이유가 바로 복원 성능 때문인데요.

 

latent dimension이 2일 때는 100 epoch 정도 돌면 대략 reconstruction error가 110 ~ 120 정도가 나옵니다.

 

하지만 latent dimension이 15일 때는 100 epoch정도 돌면 대략 reconstruction error가 65 ~ 70 정도가 나옵니다.

 

이를 통해 latent dimension이 증가할 수록 reconstruction error가 좋아질 수 있다는 사실을 확인할 수 있었습니다.

 

다만, Decoder의 성능이 안 좋아지는 문제가 있어서 약간 아쉬움이 남습니다.

 

 

 

마지막으로, latent vector의 값을 (-10, -10)부터 (10, 10)까지 균등하게 이동시키면서 복원했을 때 숫자가 어떻게 변하는지를 나타내는 그림을 보여드리고 마무리 지으려고 합니다.

 

 

 

해당 그림을 그리는 코드는, 제 Github에 올라가 있으니 확인해보시면 되겠습니다.

 

(모델과는 크게 관련이 없어 따로 해당 포스팅에서는 다루지 않습니다.)

 

 

 

개인적으로는 굉장히 관심이 있었던 모델임에도 불구하고, 생각보다 논문에 나온 것 처럼 깔끔한 성능을 내기가 너무 어려웠다는 점이 아쉬웠습니다.

 

 

모델의 구조를 이해하거나 이런 부분이 어려운건 아니였지만, 막상 구현했을 때 논문에 나온 성능이 나와야 의미가 있는 것이겠죠.

 

 

생성되는 것을 봐서는 모델 구조를 잘못 짰다거나 하진 않은 것 같은데, 쉽지 않네요 ㅠㅠ

 

 

추후에 이 문제를 해결할 수 있는 방법을 알게 된다면 해결해보도록 하겠습니다.

 

 

해당 포스팅에 사용된 코드는 다음 Github 주소에 올라가 있습니다.

 

github.com/PeterKim1/paper_code_review

 

 

 

 

 

 

오늘은 Autoencoder의 구조와 GAN framework를 결합해 탄생한 AAE에 대해서 알아보도록 하겠습니다.

 

 

paper : arxiv.org/abs/1511.05644

 

Adversarial Autoencoders

In this paper, we propose the "adversarial autoencoder" (AAE), which is a probabilistic autoencoder that uses the recently proposed generative adversarial networks (GAN) to perform variational inference by matching the aggregated posterior of the hidden co

arxiv.org

 

 

Abstract

 

 

이번 논문에서는 autoencoder의 hidden code vector의 aggregated posterior를 임의의 prior distribution과 매칭 하여 variational inference를 수행하기 위해 최근에 제안된 generative adversarial networks를 사용하는 확률적 autoencoder인 "adversarial autoencoder" (AAE)을 제안한다.

 

Aggregated posterior를 prior에 매칭시키는 것은 prior space의 특정 부분에서 생성하는 것이 의미 있는 샘플을 가져옴을 보장한다.

 

그 결과로, adversarial autoencoder의 decoder는 도입된 prior를 data distribution으로 mapping 하는 deep generative model을 학습한다.

 

우리는 semi-supervised classification, disentangling style and content of images, unsupervised clustering, dimensionality reduction and data visualization과 같은 응용 분야에서 사용될 수 있음을 보인다.

 

MNIST와 Street View House Numbers, Toronto Face dataset에서 실험을 수행하였으며, adversarial autoencoders가 generative modeling과 semi-supervised classification task에서 경쟁력 있는 결과를 나타냄을 보였다.

 

 

1. Introduction

 

 

Audio나 image, 혹은 video와 같은 풍부한 분포를 포착하기 위해 scalable generative model을 만드는 것은 machine learning의 중요한 도전 중 하나이다.

 

최근까지, Restricted Boltzmann Machines (RBM), Deep Belief Networks (DBNs), 그리고 Deep Boltzmann Machines (DBMs)와 같은 deep generative models들은 주로 MCMC 기반의 알고리즘에 의해서 학습되어 왔다.

 

이러한 접근법에서 MCMC 방법은 학습이 진행될수록 더욱 애매해지는 log-likelihood의 gradient를 계산한다.

 

이는 Markov Chains에서 나온 샘플들이 modes 사이에서 충분히 빠르게 혼합될 수 없기 때문이다.

 

최근에, direct back-propagation을 통해서 학습되고 MCMC 학습에서 마주하게 되는 어려움들을 회피할 수 있는 generative model이 개발되어져 왔다.

 

예를 들어, variational autoencoder (VAE) 혹은 importance weighted autoencoders는 잠재 변수에 대한 posterior distribution을 예측하는 recognition network를 사용하였고, generative adversarial networks (GAN)는 back-propagation을 통해서 네트워크의 output distribution을 직접적으로 형성하기 위해 adversarial training procedure를 사용했고 generative moment matching networks (GMMN)은 데이터 분포를 학습하기 위해 moment matching cost function을 사용하였다.

 

본 논문에서는 autoencoder를 generative model로 전환할 수 있는 adversarial autoencoder (AAE)라고 불리는 일반적인 접근 방식을 제안한다.

 

우리 모델에서, autoencoder는 전통적인 reconstruction error criterion와 autoencoder의 latent representation의 aggregated posterior distribution을 임의의 prior distribution으로 매칭 하는 adversarial training criteriondual objectives를 가지고 학습된다.

 

우리는 이 training criterion이 VAE 학습과 강한 연관이 있음을 보였다.

 

학습의 결과로 encoder는 data distribution을 prior distribution으로 변환하도록 학습하며 decoderimposed prior를 data distribution으로 mapping하는 deep generative model을 학습한다.

 

(이번 논문에서 가장 핵심되는 내용들이라 밑줄과 볼드체로 표시하였습니다. 해당 내용을 숙지하시고 앞으로 논문 내용을 보시면 좋을 것 같습니다.)

 

Figure 1

Figure 1: adversarial autoencoder의 아키텍처. 위의 행은 이미지 latent code $z$로부터 이미지 $x$를 복원하는 일반적인 autoencoder이다. 아래 행은 한 샘플이 autoencoder의 hidden code로부터 나온 것인지, 혹은 유저에 의해서 정해진 sampled distribution으로부터 나온 것인지를 예측하도록 학습된 두 번째 네트워크를 나타낸다.

 

 

1.1 Generative Adversarial Networks

 

 

Generative Adversarial Networks (GAN) framework는 generative model $G$와 discriminative model $D$의 두 신경망 사이의 min-max adversarial game을 만든다.

 

The discriminator model $D(x)$는 data space에 있는 point $x$가 우리의 generative model로부터 나온 샘플(negative samples)이라기보다는 우리가 모델링하려고 시도하고 있는 data distribution으로부터 나온 샘플(positive samples) 일 확률을 계산하는 신경망이다. 

 

동시에, generator는 prior $p(z)$로부터 나온 샘플 $z$를 data space로 mapping하는 함수 $G(z)$를 사용한다.

 

$G(z)$는 discriminator가 샘플들이 data distribution에서 생성되었다고 믿도록 최대한 속게끔 학습된다.

 

Generator는 $D(x)$의 $x$에 대한 gradient를 활용하여 학습하게 되며, 이를 사용해 generator의 parameter를 수정한다.

 

이 게임의 해결책은 다음과 같이 표현될 수 있다.

 

 

Generator $G$와 discriminator $D$는 두 단계에서 교대로 적용되는 SGD를 사용하여 확인할 수 있다.

 

(a) discriminator는 generator에 의해서 생성된 fake samples로부터 true samples를 구별하도록 학습한다.

 

(b) 생성된 샘플을 가지고 discriminator를 속이기 위해서 generator를 학습한다.

 

(GAN과 관련해서는 cumulu-s.tistory.com/22 에서 이미 다뤘기 때문에, 혹시 GAN에 대한 background 지식이 없으신 분들은 해당 포스트를 참고하셔서 GAN에 대한 내용을 확인하시면 되겠습니다.)

 

 

 

2. Adversarial Autoencoders

 

 

$x$를 input이라 하고 $z$를 deep encoder와 decoder를 가진 autoencoder의 latent code vector (hidden units)이라고 하자.

 

$p(z)$를 codes에 대해서 우리가 도입하길 원하는 prior distribution이라 하고, $q(z|x)$는 encoding distribution, $p(x|z)$는 decoding distribution이라 하자.

 

또한, $p_d(x)$는 data distribution이라 하고 $p(x)$는 model distribution이라 하자.

 

Autoencoder의 encoding function $q(z|x)$는 autoencoder의 hidden code vector에 대해서 aggregated posterior distribution $q(z)$를 다음과 같이 정의한다.

 

Equation 1

 

Adversarial autoencoder는 aggregated posterior $q(z)$를 임의의 prior $p(z)$에 매칭 시킴으로써 regularized 되는 autoencoder이다.

 

그러기 위해서, adversarial network는 Figure 1에서 나타난 것처럼 autoencoder의 hiddne code vector의 위쪽에 부착된다.

 

$q(z)$가 $p(z)$에 매칭되도록 안내하는 것은 adversarial network이다.

 

그동안에, autoencoder는 reconstruction error를 최소화하도록 시도한다.

 

또한, Adversarial network의 generator는 autoencoder의 encoder $q(z|x)$이다.

 

Encoder는 aggregated posterior distribution이 discriminative adversarial network가 hidden code $q(z)$이 true prior distribution $p(z)$로부터 왔다고 생각하도록 속일 수 있다는 것을 보장한다.

 

Adversarial network와 autoencoder 둘 다 reconstruction phase와 regularization phase라는 두 단계에 걸쳐 SGD를 활용해 공동으로 학습되며, mini-batch로 수행된다.

 

Reconstruction phase에서, autoencoder는 encoder와 decoder를 input의 reconsturction error를 최소화하도록 업데이트한다.

 

Regularization phase에서, adversarial network는 첫 번째로 discriminative network를 prior를 사용해서 생성된 true sample을 autoencoder에 의해서 계산된 hidden code로부터 생성된 샘플과 구별하도록 학습한다.

 

그러고 나서 autoencoder의 encoder이기도 한 generator가 discriminative network를 속이도록 업데이트한다.

 

한 번 학습 절차가 마무리되면, autoencoder의 decoder는 imposed prior $p(z)$를 data distribution으로 mapping 하는 generative model을 정의할 수 있다.

 

Adversarial autoencoder의 encoder $q(z|x)$에 대한 여러 가지 가능한 선택이 존재한다.

 

(1) Deterministic: 이 경우 우리는 $q(z|x)$를 $x$의 deterministic function이라고 가정한다. 이 경우에, encoder는 standard autoencoder의 encoder와 유사하고, $q(z)$에서 확률성의 유일한 원천은 data distribution $p_d(x)$이다.

 

(2) Gaussian posterior: 이 경우 우리는 $q(z|x)$를 Gaussian distribution이라고 가정하며, 이 분포의 평균과 분산은 encoder network에 의해서 예측된다: $z_i \sim N(\mu_i(x), \sigma_i(x))$.

 

이 경우에, $q(z)$에서의 확률성은 encoder의 output에서의 데이터 분포와 Gaussian distribution의 무작위성 모두로부터 나온다. 

 

우리는 encoder network를 통한 back-propagation을 위해 VAE의 re-parametrization trick을 사용할 수 있다.

 

(3) Universal approximator posterior: Adversarial autoencoder는 $q(z|x)$를 posterior의 universal approximator로 학습하는 데 사용될 수 있다.

 

Adversarial autoencoder의 encoder network를 input $x$와 고정된 분포(예를 들어, Gaussian)를 가지는 random noise $\eta$를 입력으로 받는 함수 $f(x, \eta)$라고 가정하자.

 

우리는 $\eta$의 다른 샘플에 대해서 $f(x, \eta)$를 평가함으로써 임의의 posterior distribution $q(z|x)$에서 샘플링할 수 있다.

 

다르게 말해서, 우리는 $q(z|x, \eta) = \delta(z - f(x,\eta))$로 가정할 수 있고 posterior $q(z|x)$와 aggregated posterior $q(z)$는 다음과 같이 정의된다.

 

 

이 경우에, $q(z)$에서의 확률성은 encoder의 input에서 데이터 분포와 random noise $\eta$ 모두에게서 나오며, posterior $q(z|x)$는 더 이상 Gaussian으로 제약되지 않고 encoder는 input $x$가 주어졌을 때 임의의 posterior distribution을 학습할 수 있다.

 

Aggregated posterior $q(z)$로부터 샘플링을 하는 효율적인 방법이 있기 때문에, adversarial training procedure는 encoder network $f(x, \eta)$를 통해서 direct back-propagation을 통해 $q(z)$를 $p(z)$에 매칭 할 수 있다.

 

 

다른 종류의 $q(z|x)$를 선택하는 것은 다른 training dynamics를 가지는 다른 종류의 모델을 야기할 것이다.

 

예를 들어, $q(z|x)$의 deterministic case에서, network는 오직 데이터 분포의 확률성을 이용하는 것을 통해 $q(z)$를 $p(z)$로 매칭해야 한다.

 

하지만 데이터의 empirical distribution이 training set에 의해서 고정되고 mapping이 deterministic 하기 때문에, 이는 매우 smooth하지 않은 $q(z)$를 만들게 될 것이다.

 

그러나, Guassian이나 universal approximator case에서 network는 $q(z)$를 smoothing out 함으로써 adversarial regularization stage에서 도움이 될 수 있는 확률성의 추가적인 원천에 접근할 수 있다.

 

그럼에도 불구하고, 광범위한 hyper-parameter search 후에, 우리는 $q(z|x)$의 각 유형에 대한 유사한 test-likelihood를 얻었다.

 

따라서 논문의 나머지에서 우리는 $q(z|x)$의 deterministic version에 대한 결과만 보고한다.

 

 

2.1 Relationship to Variational Autoencoders

 

 

우리의 연구는 spirit(정신?)에서 variational autoencoder와 유사하다.

 

하지만 VAE는 autoencoder의 hidden code vector에 prior distribution을 도입하기 위해 KL divergence penalty를 사용하였으나, 우리는 hidden code vector의 aggregated posterior를 prior distribution과 일치시킴으로써 adversarial training procedure를 사용한다.

 

VAE는 다음의 $x$의 negative log-likelihood에 대한 upper-bound를 최소화하였다.

 

Equation 2

 

Aggregated posterior $q(z)$는 Equation 1에 정의되었으며, $q(z|x)$를 Gaussian 분포라고 가정하고 $p(z)$를 임의의 분포라고 가정한다.

 

Variational bound는 3개의 term을 포함한다. 

 

첫 번째 term은 autoencoder의 reconstruction term으로 볼 수 있으며, 두 번째와 세 번째는 regularization term으로 볼 수 있다.

 

Regularization term이 없다면, 모델은 단순히 input을 복원하는 standard autoencoder이다.

 

하지만, regularization term의 존재로, VAE는 $p(z)$와 호환이 되는 latent representation을 학습한다.

 

Cost function의 두 번째 term은 posterior distribution의 큰 variance를 유도하지만, 세 번째 term은 prior $p(z)$와 aggregated posterior $q(z)$사이의 cross-entropy를 최소화한다.

 

Equation (2)에서 KL divergence나 cross-entropy term은 $q(z)$가 $p(z)$의 mode를 선택하도록 유도한다.

 

Adversarial autoencoder에서, 두 번째 2 개의 term을 $q(z)$가 $p(z)$의 전체 분포에 일치하도록 유도하는 adversarial training procedure로 대체한다.

 

Figure 2

Figure 2: MNIST에서 adversarial autoencoder와 variational autoencoder의 비교. Adversarial autoencoder에 대해서 hold-out image(Test set을 의미한다고 보시면 됩니다.) 들의 hidden code $z$를 (a) 2-D Gaussian에 fitting 시켰을 때와 (b) 2-D Gaussian의 10개의 mixture에 fitting 시켰을 때의 결과이다. 각 색깔은 관련된 label을 나타낸다. Variational autoencoder에도 동일하게 (c) 2-D Gaussian과 (d) 10개의 2-D Gaussian의 혼합에 fitting 시킨 결과이다. (e) 2-D Gaussian adversarial autoencoder에서 각 hidden code dimension $z$를 따라서 Gaussian 백분위수를 균일하게 샘플링해 만들어진 이미지이다.

 

이번 section에서, code distribution에 대해 지정된 prior distribution $p(z)$를 도입하는 adversarial autoencoder의 능력을 VAE와 비교한다. 

 

Figure 2a는 hidden code $z$에 구형의 2-D Gaussian prior distribution이 부과되는 MNIST 숫자 데이터에 훈련된 adversarial autoencoder에서 비롯되는 test data의 coding space $z$를 보여준다.

 

Figure 2a에 나타난 학습된 manifold는 sharp transition을 드러내고 있으며, 이는 coding space가 채워져 있고 구멍이 없음을 나타낸다. 

 

실제로, coding space에서의 sharp transition은 $z$내에서 interpolate 해서 만들어진 이미지들이 data manifold (Figure 2e)에 놓인다는 것을 나타낸다.

 

대조적으로, Figure 2c는 adversarial autoencoder 실험에서 사용된 것과 동일한 architecture를 가지는 VAE의 coding space를 보여준다.

 

이 경우에 VAE가 대략 2-D Gaussian distribution의 모습과 일치함을 볼 수 있다.

 

하지만, coding space의 여러 local 지역에 mapping 된 데이터 포인트가 없는 것은 VAE가 adversarial autoencoder와 같이 data manifold를 포착하지 못함을 나타낸다.

 

Figure 2b와 2d는 도입된 분포가 2-D Gaussian의 10개 mixture일 때 adversarial autoencoder와 VAE의 code space를 보여준다.

 

Adversarial autoencoder는 성공적으로 aggregated posterior를 prior distribution에 일치시켰다. (Figure 2b)

 

대조적으로, VAE는 10개의 Gaussian mixture와 체계적인 차이를 보여주고 있으며, 이는 위에서 설명한 것과 같이 VAE가 distribution의 mode를 매칭 시키는데 집중하기 때문인 것을 나타낸다.

 

VAE와 adversarial autoencoder 사이의 중요한 차이점은, VAE에서는 Monte-Carlo sampling에 의해서 KL Divergence를 통해 back-propagate 하기 위해 prior distribution의 정확한 함수 형태를 알고 있어야 한다.

 

하지만, AAE에서는 $q(z)$를 $p(z)$와 일치하도록 유도하기 위해서 prior distribution으로부터 샘플링하는 것이 가능하기만 하면 된다.

 

Section 2.3에서, adversarial autoencoder가 distribution의 정확한 함수 형태를 모르더라도 Swiss roll 분포와 같은 복잡한 분포를 도입할 수 있음을 보인다.

 

 

2.2 Relationship to GANs and GMMNs

 

이 부분은 생략합니다. 

 

 

 

2.3 Incorporating label information in the Adversarial Regularization

 

 

데이터가 라벨링이 되어 있는 경우에, hidden code의 분포를 더 잘 만들어 내기 위해 adversarial training stage에서 label information을 포함시킬 수 있다.

 

이번 section에서, autoencoder의 latent representation을 더욱 강하게 regularize 하기 위해 부분적인 혹은 완전한 label information을 어떻게 활용할 수 있는지를 설명한다.

 

이 아키텍처를 설명하기 위해서, adversarial autoencoder를 2-D Gaussian의 10개 mixture에 fitting 시킨 Figure 2b로 돌아간다.

 

이제 우리는 Gaussian distribution의 mixture의 각 mode가 MNIST의 label을 나타내도록 강제하는 것을 목표로 한다.

 

 

Figure 3는 이러한 semi-supervised 접근법을 위한 학습 절차를 나타낸다.

 

우리는 distribution의 mode와 label을 연관시키기 위해서 discriminative network의 input에 one-hot vector를 추가한다.

 

One-hot vector는 class label이 주어졌을 때 이에 대응되는 discriminative network의 decision boundary를 선택하는 switch로 작용한다.

 

이 one-hot vector는 unlabeled examples에 대해서 extra class를 가진다.

 

예를 들어, 2-D Gaussian의 10개 mixture를 도입하는 경우(Figure 2b and 4a)에 one-hot vector는 11개의 클래스를 포함한다.

 

첫 번째 10개의 class 각각은 대응되는 각각의 mixture component에 대한 decision boundary를 선택한다.

 

One-hot vector에 있는 여분의 class는 unlabeled training point에 대응된다.

 

Unlabeled point가 모델에 제시되었을 때, extra class는 켜지고, Gaussian distribution의 전체 mixture를 위한 decision boundary를 선택한다.

 

Adversarial training의 positive phase 동안에, one-hot vector를 통해 discriminator에 positive sample이 뽑힌 mixture component의 label을 제공한다.

 

Unlabeled examples를 위해 제공되는 positive samples는 특정 class가 아닌 Gaussian의 전체 mixture에서 나온다.

 

Negative phase 동안에, training point image의 label을 one-hot vector를 통해서 discriminator에 제공한다.

 

Figure 4

Figure 4: hidden code를 더 잘 규제하기 위해 label information을 활용한 경우. Top Row: coding space가 10개의 2-D Gaussian mixture와 일치하기 위해서 학습된 경우: (a) hold-out image의 coding space z (b) 첫 번째 3 mixture component의 manifold: 각 패널은 대응되는 mixture component의 축을 따라 Gaussian percentile을 균등하게 샘플링했을 때 생성되는 이미지들을 포함한다. Bottom Row: 동일하지만 swiss roll distribution일 때. Label은 숫자 순서대로 mapping 됨. (즉, swiss roll의 첫 번째 10%는 숫자 0에 대응되며, 나머지는 동일) (c) hold-out image의 coding space z (d) main swiss roll axis를 따라 걸으면서 생성된 샘플들

 

Figure 4a는 1만 개의 labeled MNIST와 4만 개의 unlabeled MNIST 예제에 대해서 훈련된 10개의 2-D Gaussian의 혼합을 prior로 훈련된 adversarial autoencoder의 latent representation을 나타낸다.

 

이 경우에, prior의 $i$번째 mixture components는 semi-supervised 방식으로 $i$번째 class에 부여된다.

 

Style representation은 각 mixture component 내에서는 일관적으로 표현되며, 이것은 class와 독립이다.

 

예를 들어, Figure 4b의 모든 패널의 왼쪽 상단 영역은 곧게 쓰인 style에 대응되고, 오른쪽 하단 영역은 기울어진 스타일에 대응된다.

 

이 방법은 MNIST data를 "swiss roll" (평균이 swiss roll 축의 길이를 따라서 균등하게 분포되는 조건부 Gaussian distribution)에 mapping 하는 것을 통해서 검증되는 것처럼 parametric form이 없는 임의의 분포에도 확장될 수 있다.

 

Figure 4c는 coding space z를 묘사하고 있으며, Figure 4d는 latent space에서 swiss roll axis를 따라서 걸음으로써 생성되는 이미지들을 강조한다.

 

 

 

3. Likelihood Analysis of Adversarial Autoencoders

 

해당 논문에서 사용하고 있는 metric이 GAN paper에서 사용된 metric과 유사한데, 요즘은 잘 사용하지 않는 것 같아서 해당 부분도 제외합니다.

 

 

4. Supervised Adversarial Autoencoders

 

 

semi-supervised learning은 machine learning에서 오랫동안 지속되어 온 개념적인 문제이다.

 

최근에, 생성 모델들은 원칙적인 방식으로 class label information을 많은 다른 잠재적인 variation의 요소로부터 분리할 수 있기 때문에 semi-supervised learning에 대한 가장 인기 있는 접근법 중 하나가 되었다.

 

 

이번 section에서는, 먼저 fully supervised scenarios에 집중하고, 이미지 style information으로부터 class label information을 분리할 수 있는 adversarial autoencoder의 구조에 대해서 논의해본다.

 

그러고 나서 이 구조를 Section 5에서의 semi-supervised setting으로 확장한다.

 

Label information을 포함시키기 위해서, 우리는 decoder에 label의 one-hot vector encoding을 제공하기 위해 Figure 1의 네트워크 아키텍처를 Figure 6의 형태로 변경한다.

 

Decoder는 label을 확인하는 one-hot vector와 이미지를 reconstruct 하는 hidden code z를 모두 활용할 수 있다.

 

이 아키텍처는 hidden code z에서 label과 독립적인 모든 정보를 네트워크가 유지하도록 강제한다.

 

Figure 6
Figure 7

Figure 7a는 hidden code가 15-D Gaussian이도록 설정된 네트워크를 MNIST 숫자에 학습되었을 때의 결과를 보여준다. 

 

Figure 7a의 각 행은 hidden code $z$가 특정한 값으로 고정되지만 레이블이 변하면서 만들어진 복원된 이미지를 나타낸다.

 

주어진 행에 대해서 복원된 이미지의 style은 일정하다.

 

Figure 7b는 같은 실험을 Street View House Numbers dataset에 적용한 결과를 보여준다. SVHN style manifold를 보여주는 비디오는 다음 주소에서 볼 수 있다. (주소가 있긴 했는데, 눌러보니 안 되네요. 아마 링크가 변한 것 같아요.)

 

 

이번 실험에서, one-hot vector는 이미지에서 central digit과 연관된 라벨을 나타낸다.

 

각 row에서 style information은 가장 왼쪽과 가장 오른쪽 숫자의 라벨과 관련된 정보를 포함하게 되는데, 이는 맨 왼쪽과 맨 오른쪽 숫자는 one-hot encoding으로 label information이 제공되지 않기 때문이다.

 

 

5. Semi-supervised Adversarial Autoencoders

 

 

Section 4를 기반으로 해서, labeled data만을 사용해서 얻을 수 있는 분류 성능을 unlabeled data의 generative description을 활용하여 향상하는 semi-supervised learning을 위한 모델을 개발하기 위해 adversarial autoencoder를 사용한다.

 

구체적으로 말하자면, 데이터가 Categorical distribution에서 만들어진 latent class variable $y$와 Gaussian distribution에서 만들어진 연속형 잠재 변수 $z$에 의해서 생성되었다고 가정한다.

 

Figure 8

 

우리는 Figure 6의 네트워크 구조를 바꿔서 AAE의 inference network가 encoder $q(z, y|x)$를 사용하여 discrete class variable $y$와 연속형 잠재 변수 $z$를 예측하도록 만든다. (Figure 8 참조)

 

Decoder는 one-hot vector로 되어 있는 class label과 연속형 hidden code $z$를 활용하여 이미지를 복원한다.

 

여기에는 autoencoder의 hidden representation을 규제하기 위한 두 개의 별도의 adversarial network가 존재한다.

 

첫 번째 adversarial network는 Categorical distribution을 label representation으로 강제한다.

 

이 adversarial network는 latent class variable $y$가 어떠한 style information도 가지지 않도록 보장하며, $y$의 aggregated posterior distribution이 Categorical distribution과 매칭 된다는 것을 보장한다.

 

두 번째 adversarial network는 Gaussian distribution을 style representation으로 강제하며 이는 latent variable $z$가 연속형 Gaussian variable임을 보장한다.

 

 

Adversarial network 양쪽 모두와 autoencoder는 SGD를 이용해 reconstruction phase, regularization phase, and semi-supervised classification phase로 이루어진 3 phase 동안 공동으로 학습된다.

 

1) Reconstruction phase에서, autoencoder는 encoder $q(z, y|x)$와 decoder를 unlabeled mini-batch에서 input의 reconstruction error를 최소화하도록 업데이트한다.

 

2) Regularization phase에서, 각 adversarial network는 첫 번째로 각자의 discriminative network가 실제 샘플(Categorical and Gaussian prior를 사용해서 만들어진)을 생성된 샘플(autoencoder에 의해서 계산된 hidden codes)로부터 구별하도록 업데이트한다.

 

Adversarial networks는 그다음으로 그들의 discriminative networks를 혼란스럽게 만들도록 generator를 업데이트한다.

 

3) Semi-supervised classification phase에서, autoencoder는 $q(y|x)$가 labeled mini-batch에서 cross-entropy cost를 최소화하도록 학습된다.

 

Table 2

 

MNIST와 SVHN에 대한 semi-supervised classification 실험의 결과는 Table 2에서 확인할 수 있다.

 

100개와 1000개의 label을 가지는 MNIST dataset에 대해서, AAE의 성능은 VAE에 비해서 상당히 좋았으며, VAT와 CatGAN과 비슷하였고, 하지만 Ladder networks와 ADGM가 더 좋은 성능을 냈다.

 

우리는 또한 supervised AAE를 모든 이용 가능한 label에 학습시켰으며 error rate 0.85%를 얻었다.

 

이와 비교해서, 같은 구조를 가지고 있는 dropout supervised neural network는 전체 MNIST dataset에 대해서 1.25%의 error rate를 나타냈으며, 이는 adversarial training의 regularization effect를 강조한다.

 

1000개의 label을 가지는 SVHN dataset에 대해서는, AAE는 ADGM에 의해 나타난 SOTA classification performance와 거의 일치했다.

 

또한 모든 AAE model은 end-to-end로 학습되었다는 것을 언급할 가치가 있으며, 반면에 semi-supervised VAE model은 한 번에 한 layer씩 학습되어야 한다.

 

 

6. Unsupervised Clustering with Adversarial Autoencoders

 

 

이전 section에서는, 제한된 label information을 가지고 adversarial autoencoder가 강력한 semi-supervised representations을 학습할 수 있음을 보였다.

 

하지만, 어떠한 supervision 없이 unlabeled data으로부터 "강력한" representation을 학습할 수 있는지 없는지에 대한 질문은 아직도 답변되지 않은 채로 남아있다.

 

이번 section에서는, adversarial autoencoder가 순수하게 unsupervised fashion에서 연속적인 latent style variable로부터 discrete class variable을 구분할 수 있는지를 보인다.

 

우리가 사용한 아키텍처는 Figure 8와 유사하지만, 차이가 있다면 semi-supervised classification stage를 제거했다는 점이다.

 

따라서, 더 이상 이 모델을 labeled mini-batch에 학습시키지 않아도 된다.

 

또 다른 차이점은 inference network $q(y|x)$가 데이터가 모이기를 바라는 범주의 수를 차원으로 갖는 one-hot vector를 예측한다는 점이다.

 

Figure 9

 

 

Figure 9은 cluster의 수를 16으로 했을 때 MNIST에 학습된 AAE의 unsupervised clustering의 성능을 나타낸 것이다.

 

각 행은 하나의 cluster에 대응된다.

 

각 행의 첫 번째 이미지는 cluster head를 나타내며, 이는 style variable을 0으로 고정하고 label variable을 16 one-hot vector 중 한 개로 설정했을 때 만들어지는 숫자이다.

 

각 행에서 나머지 이미지들은 $q(y|x)$를 기반으로 대응되는 카테고리로 분류된 랜덤 테스트 이미지들이다.

 

 

우리는 AAE가 class label로 어떤 discrete style을 선택한다는 것을 볼 수 있다.

 

예를 들어서, 기울어진 숫자 1과 6 (cluster 16과 11)은 곧게 선 1과 6 (cluster 15와 10)과 별개의 군집에 놓여 있으며, 혹은 네트워크는 숫자 2를 이를 고리 모양으로 쓰였는지 아닌지에 따라 두 개의 군집(cluster 4, 6)으로 분리했다.

 

 

Table 3

우리는 AAE의 unsupervised clustering 성능을 평가하기 위해 실험을 수행했으며, 다음과 같은 평가 protocol을 사용했다.

 

학습이 끝난 후에, 각 cluster $i$에 대해서, $q(y_i|x_n)$을 최대화하는 validation example $x_n$을 찾고, $x_n$의 label을 cluster $i$의 모든 포인트에 적용한다. 그러고 나서 각 cluster에 assign 된 class label을 기반으로 test error를 계산한다.

 

Table 3에서 보이는 것처럼, AAE는 16개, 30개의 전체 라벨을 가지는 경우 9.55%와 4.10%의 classification error rate를 나타냈다. 

 

cluster의 수가 증가할수록 classification rate가 증가함을 확인하였다.

 

 

7. Dimensionality Reduction with Adversarial Autoencoders

 

 

고차원 데이터의 시각화는 데이터 생성 과정에 대한 이해를 촉진시키고 데이터에 대한 유용한 정보를 추출하도록 해준다는 점에서 많은 applications에서 매우 중요한 문제이다.

 

가장 흔하게 사용되는 데이터 시각화 방법은 근처의 point가 유사한 object에 대응되도록 저차원의 embedding을 학습하는 것이다.

 

지난 수십 년간, t-SNE와 같은 non-parametric dimensionality reduction techniques들이 계속해서 제안되어 왔다.

 

이러한 방법들의 주요한 문제점은 새로운 데이터 포인트의 embedding을 찾는 데 사용될 수 있는 parametric encoder를 가지고 있지 않다는 점이다.

 

Parametric t-SNE와 같은 다른 방법들은 이 문제를 해결하기 위해 제안되었다.

 

 

Autoencoder는 그러한 embedding을 위해 요구되는 non-linear mapping을 제공하기 때문에 흥미로운 대안이 된다.

 

하지만, non-regularized autoencoder는 manifold를 많은 다른 domain으로 균열시킨다는 사실이 널리 알려져 있으며, 이는 유사한 이미지에 대해서 매우 다른 code를 갖도록 만든다.

 

 

이번 section에서, 우리는 dimensionality reduction과 data visualization 목적을 위한 adversarial autoencoder 아키텍처를 제시한다.

 

이러한 autoencoder에서, adversarial regularization이 유사한 이미지들의 hidden code를 서로 붙게끔 만든다는 것을 보여줄 것이며, 따라서 autoencoder에 의해서 학습된 embedding에서 전형적으로 마주하게 되는 manifold fracturing problem을 방지한다는 것을 보여줄 것이다.

 

Figure 10

 

우리가 $m$ class label을 가지는 데이터셋을 가지고 있고 데이터셋의 차원을 $n$으로 줄이려고 한다고 가정하자.

 

시각화를 목적으로 할 때는 $n$은 보통 2나 3이 된다. 

 

Figure 8의 아키텍처를 final representation을 $n$차원의 style representation과 cluster head의 $n$차원의 distributed representation을 추가하여서 얻게 되는 Figure 10 아키텍처로 변경한다.

 

Cluster head representation은 $m$차원의 one-hot class label vector를 $m \times n$ matrix $W_C$를 곱해서 얻게 되며, $W_C$의 행들은 SGD에 의해서 학습된 $m$ cluster head representation을 나타낸다.

 

우리는 모든 두 cluster head 간의 Euclidean distance에 처벌을 가하기 위해 추가적인 cost function을 도입한다.

 

구체적으로, 만약 Euclidean distance가 threshold $\eta$보다 크다면, cost function은 0이며, 만약 $\eta$보다 작다면, cost function은 선형적으로 distance에 처벌을 가한다.

 

Figure 11

 

Figure 11(a, b)는 $n =2$차원으로 MNIST dataset ($m= 10$)에 대해 1000개와 100개의 label을 가지고 semi-supervised dimensionality reduction을 한 결과를 나타낸다.

 

network가 digit cluster를 깔끔하게 분리한다는 것을 확인할 수 있으며 각각 semi-supervised classification error 4.20%와 6.08%를 달성하였다.

 

2D 제약 때문에 classification error는 고차원의 경우보다 좋지 못하다. 그리고 각 cluster의 style distribution은 Gaussian이 아니다.

 

Figure 11c는 $n=2$차원에서 cluster의 수를 $m=20$으로 뒀을 때 unsupervised dimensionality reduction의 결과를 보여준다.

 

네트워크가 digit cluster와 sub-custer의 오히려 더 깔끔한 분리를 달성할 수 있음을 볼 수 있다.

 

예를 들어서, 네트워크는 숫자가 곧게 서 있는지 혹은 기울어져 있는지에 따라서 녹색 cluster인 두 개의 별개의 cluster로 숫자 1을 배치했다.

 

네트워크는 또한 숫자 6을 검은색 cluster인 3개의 cluster로 숫자가 얼마나 기울어졌는지에 따라 다르게 배치했다. 

 

또한 네트워크는 숫자 2를 빨간 cluster인 2개의 별도의 cluster로 고리 형태로 쓰였는지 아닌지에 따라 배치했다.

 

 

Figure 10에 나타난 AAE 아키텍처는 또한 이미지를 더 높은 차원($n$ > 2)으로 embedding 할 때 사용될 수 있다.

 

예를 들어서, Figure 11d는 $n = 10$차원일 때 100개의 label을 가질 때의 semi-supervised dimensionality reduction의 결과를 보여준다.

 

이 경우에, 우리는 $W_C$ matrix를 $W_C = 10I$로 고정하였으며, 이에 따라 cluster head는 10차원 simplex의 모서리에 위치한다.

 

Style representation은 10D Gaussian distribution에서 표준편차 1을 가지고 학습되었으며, final representation을 만들기 위해 cluster head에 추가되었다.

 

한번 네트워크가 학습되면, 10차원의 학습된 representation을 시각화하기 위해서 cluster head가 2D circle에 균일하게 위치한 points로 mapping 되도록 10D representation을 2D space로 mapping 하는 linear transformation을 사용하였다.

 

우리는 이 figure로부터 고차원의 경우에, style representation이 실제로 Gaussian distribution을 학습한다는 사실을 검증할 수 있었다.

 

전체 100개의 label을 가지는 경우에, 이 모델은 3.90%의 classification error-rate를 나타냈으며, 이는 Figure 8에 나온 concatenated style and label representation을 가지는 AAE 아키텍처에 의해서 달성된 1.90% classification error-rate보다 더 안 좋은 결과이다.

 

 

 

8. Conclusion

 

 

본 논문에서는 확률론적 autoencoders의 이산형 및 연속형 잠재 변수에 대한 variational inference algorithm으로 GAN framework를 사용할 것을 제안하였다.

 

우리의 방법은 adversarial autoencoder (AAE)라고 불리며, 이는 MNIST와 TFD에서 경쟁력 있는 test likelihood를 성취한 generative autoencoder이다.

 

우리는 이 방법이 어떻게 semi-supervised scenarios로 확장될 수 있는지에 대해서 논의하였으며, MNIST와 SVHN dataset에서 경쟁력 있는 semi-supervised classification performance를 나타냄을 보였다.

 

마지막으로, 이미지의 스타일과 내용을 구분하는 것, 비지도 군집화, 차원 축소 그리고 데이터 시각화에서의 adversarial autoencoder의 활용을 설명했다.

 

 

 

여기까지 해서 AAE 논문의 내용을 마무리하였습니다.

 

우리가 일반적으로 생각하는 Basic AAE의 내용만 찾으신다면 챕터의 앞부분만 읽으시면 충분하지만, 논문에서는 AAE를 활용할 수 있는 다양한 방법들을 제시하고 있어서 모든 내용을 다 다루는 것이 좋겠다고 판단, 모든 내용을 담았습니다.

 

이렇게 논문에서는 많은 활용 방법들을 제시하였는데, 논문의 공식 코드가 없다 보니 Github에 공개된 대부분의 코드들은 Basic AAE 정도만 다루고 있다는 점에서 아쉬움이 남습니다.

 

다음 글에서는 Basic AAE의 코드를 직접 작성해보면서 실험을 진행해보겠습니다.

 

+ Recent posts