Deep Convolutional Generative Adversarial Nets (DCGAN)

 

Vanila GAN의 개념

확률 분포

  • 확률변수 : 무작위 실험을 했을 때, 특정 확률로 발생하는 각각의 결과를 수치적 값으로 표현하는 변수

    • 주사위를 던졌을 때 나올 수 있는 확률변수 : 1, 2, 3, 4, 5, 6
  • 확률분포 : 확률변수가 특정한 값을 가질 확률을 나타내는 함수

    Screen Shot 2021-09-23 at 5.07.53 PM

    • 이산확률분포 : 대표적으로 주사위 눈금의 확률 분포

      1-52 screenshot

    • 연속확률분포 : 대표적으로 정규 분포(Gaussian distribution)

      2-28 screenshot

Image Data에 대한 확률분포

  • Image Data는 다차원의 feature space의 한 점으로 표헌됨

  • 사람의 얼굴에는 통계적인 평균치가 존재할 수 있음

    • ex. 눈의 길이, 코의 길이, 목의 두께, 눈썹의 길이 등등
  • Image Data에 대한 확률분포 : 이미지에서의 다양한 feature들이 각각의 확률변수가 되는 분포

    6-35 screenshot

Vanila GAN 구조

  • 머신러닝은 크게 3가지 개념 지도학습/강화학습/비지도학습 으로 분류되는데, GAN은 ‘비지도 학습’에 해당됨

ganPicture

Generator

  • Generator는 학습 데이터의 분포(distribution)를 알아내도록 학습함.

  • Generator가 학습이 잘 됬을 경우, 확률값이 높은 부분의 변수(이미지)를 sampling할 때 있을 법한 이미지가 나오게됨

    8-46 screenshot

    • Generator가 분포를 잘 학습한 다음에 확률이 높은 부분 부터 출발해서 약간의 noise를 섞어가면서 랜덤하게 sampling을 한다면 다양한 형태로 그럴싸한 image들을 생성할 수 있음

Discriminator

  • Discriminator는 실제 데이터인지 가짜 데이터인지 구별해서 각각에 대한 확률을 추정함

GAN을 통해 이루고자 하는 목표 :

  1. Generator의 분포가 원본 학습데이터의 분포를 잘 따를 수 있도록 만드는 것
  2. Discriminator가 학습이 다 된 경우 가짜 이미지와 진짜 이미지를 더 이상 구분할 수 없어서 항상 1/2 (50%) 이라는 값을 output하도록 하는 것

Vanila GAN의 문제점

  • Vanila GAN은 단순한 image (MNIST, CIFAR-10)는 괜찮은 image를 생성하였지만 복잡한 영상에 대해서는 좋은 image를 생성하지 못함
  • Training시 안정성이 떨어지는 문제가 존재함

DCGAN 구조

DCGAN의 Guidelines

Screen Shot 2021-09-17 at 5.14.44 PM

  • Generator
    • Pooling Layer 대신 Fractional-Strided Convolution을 사용
    • Batch Normalization을 적용
    • Fully Connected Hidden layer를 제거
    • Activation Funciton을 Output Layer에선 Tanh를 사용하고 나머지 Layer는 ReLU를 사용
  • Discriminator
    • Pooling Layer 대신 Strided Convolution을 사용
    • Batch Normalization을 적용
    • Fully Connected Hidden layer를 제거
    • Activation Function을 Leaky ReLU 사용

:warning: Generator의 output layer와 Discriminator의 input layer에는 Batch Normalization을 넣지 않았다

DCGAN 모델 구조

  • Generator

    Screen Shot 2021-09-19 at 3.55.12 PM

    • Input size : (100, 1, 1) ➡️ Output size : (3, 64, 64)

    • Fractional-Strided Convolution (Transposed Convolution) :

      • Strided Convolution : Stride값을 1이상의 정수값을 사용하면 Pooling 처럼 출력되는 Activation map의 size를 줄이는 효과를 얻을 수 있음

        convolution

      • Fractional-Strided(즉 Stride 값을 1보다 작은 분수)를 사용하는 경우 Activation map의 size를 키울수 있는 효과를 얻을 수 있음

        Transpose

        1. 새로운 parameter z, 와 p′을 계산

        2. input의 각 행과 열 사이에 z 만큼의 0을 삽입

          • 이 단계에서 input의 사이즈를 (2∗input−1) x (2∗input−1)만큼 증가시킴
        3. 전 단계에서 변형된 input에다 p′ 만큼의 0을 padding 해줌

        4. 전 단계에서 변형된 input에다 stride 1인 일반적인 convolution을 진행

      • Pooling 등을 통해 이미지를 축소시키면서 데이터를 압축했다면, 그 데이터를 다시 원래의 이미지로 복원하기 위해 이미지를 크게 만들어야(Up-sampling)하는데, 이럴 때 사용되는 방법

      • 단순히 이미지의 해상도를 높이는 Super resolution 에서도 많이 쓰인다

    • Code (Pytorch) :

      import torch
      import torch.nn as nn
          
      class Generator(nn.Module):
          def __init__(self, z_dim, channels_img, features_g):
              super(Generator, self).__init__()
              self.gen = nn.Sequential(
                  # input shape : z_dim * 1 * 1
                  self._block(z_dim, features_g*16, 4, 1, 0), # become 4 * 4
                  self._block(features_g * 16, features_g * 8, 4, 2, 1),  # become 8 * 8
                  self._block(features_g * 8, features_g * 4, 4, 2, 1),  # become 16 * 16
                  self._block(features_g * 4, features_g * 2, 4, 2, 1),  # become 32 * 32
                  nn.ConvTranspose2d(
                      features_g * 2, channels_img, kernel_size=4, stride=2, padding=1
                  ),
                  nn.Tanh(),
              )
          
          def _block(self, in_channels, out_channels, kernel_size, stride, padding):
              return nn.Sequential(
                  nn.ConvTranspose2d(
                      in_channels,
                      out_channels,
                      kernel_size,
                      stride,
                      padding,
                      bias=False,
                  ),
                  nn.BatchNorm2d(out_channels),
                  nn.ReLU(),
              )
          
          def forward(self, x):
              return self.gen(x)
      
  • Discriminator

    Screen Shot 2021-09-19 at 3.55.22 PM

    • Input : (3, 64, 64) ➡️ Output : (1, 1, 1)

    • Code (Pytorch) :

      import torch
      import torch.nn as nn
          
      class Discriminator(nn.Module):
          def __init__(self, channels_img, features_d):
              super(Discriminator, self).__init__()
              self.disc = nn.Sequential(
                  # input shape : channels_img * 64 * 64
                  nn.Conv2d(
                      channels_img, features_d, kernel_size=4, stride=2, padding=1
                  ), # become 32 * 32
                  nn.LeakyReLU(0.2),
                  self._block(features_d, features_d*2, 4, 2, 1),   # become 16 * 16
                  self._block(features_d*2, features_d*4, 4, 2, 1), # become 8 * 8
                  self._block(features_d*4, features_d*8, 4, 2, 1), # become 4 * 4
                  nn.Conv2d(features_d*8, 1, kernel_size=4, stride=2, padding=0), # become 1 * 1
                  nn.Sigmoid(),
              )
          
          def _block(self, in_channels, out_channels, kernel_size, stride, padding):
              return nn.Sequential(
                  nn.Conv2d(
                      in_channels,
                      out_channels,
                      kernel_size,
                      stride,
                      padding,
                      bias=False,
                  ),
              nn.BatchNorm2d(out_channels),
              nn.LeakyReLU(0.2),
              )
          
          def forward(self, x):
              return self.disc(x)
      

DCGAN 훈련

Weight Initialization

  • All weights were initialized from a zero-centered Normal distribution with standard deviation 0.02

  • Code (Pytorch) :

    def initialize_weights(model):
        for m in model.modules():
            if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d, nn.BatchNorm2d)):
                nn.init.normal_(m.weight.data, 0.0, 0.02)
    

Hyperparameters

  • LEARNING_RATE = 2e-4 : We found the suggested learning rate of 0.001, to be too high, using 0.0002 instead.

  • BATCH_SIZE = 128 : 모든 모델은 mini-batch stochastic gradient descent(SGD)에서 batch size 128로 훈련됨

  • nn.LeakyReLU(0.2) : Leaky ReLU의 기울기는 0.2

  • hyperpameter를 변경한 Adam optimizer를 사용함.

    optimizer_gen = optim.Adam(gen.parameters(), lr=LEARNING_RATE, betas=(0.5, 0.999))
    optimizer_disc = optim.Adam(disc.parameters(), lr=LEARNING_RATE, betas=(0.5, 0.999))
    
    • learning rate 0.001은 너무 높고 0.0002가 적당하다는 점을 찾았고,
    • momentum B1은 0.9에서 트레이닝이 변동성이 크고 불안정한 반면 0.5에서 트레이닝이 안정적이었다.

Training Algorithm

  • Batch와 Mini-batch gradient descent에 대한 개념 정리 : Blog

  • MinMax Loss :

    vanilaGAN

    • Ex~pdata(x)[logD(x)] 는 training data(real data) x를 Discriminator에 넣었을 때 나오는 결과를 log를 취했을 때 얻는 기댓(평균)값을 의미한다.
    • Ez~px(z)[log(1-D(G(z)))] 는 noise distribution z를 Generator에 넣었을 때 나오는 결과(fake image)를 Discriminator에 넣는다. 그 결과를 log (1 - Discriminator 결과값)를 했을 때 얻는 기댓(평균)값을 의미한다.
    • 만약에 D가 매우 뛰어난 성능을 보이고 있다고 가정한다. D에 들어온 sample x가 실제로 real_data에서 온 sample일 경우 D(x) = 1이 되므로 첫 번째 항에서 ‘log 1 = 0’ 이 되어 사라진다. 그리고 G(z)가 생성한 fake_image를 잘 판별할 수 있어서 D(G(z)) = 0이라는 결과를 나타내어 두 번째 항에서 ‘log (1 - 0) = log 1 = 0’이 되어 식 전체 값은 0이 된다. 따라서 L(D,G) = 0이 D의 입장에서 얻을 수 있는 ‘최댓값’이라는 것을 알 수 있다.
    • 반대로 이번에는 G가 아무도 못알아보게끔 image를 잘 생성한다고 가정한다. 일단 G는 첫 번째 항에는 관여할 수 없다. 오로지 D가 real_data로 부터 나온 sample x를 얼마나 잘 판별하는지에 대한 항이기 때문이다. G의 입장에서 확인해야할 항은 두 번째 항이다. G가 매우 뛰어난 성능을 보여 D를 무조건 속인다고 가정했기 때문에 D(G(z))에서 D는 real이라고 판단해서 1의 결론을 내놓습니다. 따라서 ‘log (1 - 1) = -∞’ 라는 결과가 나오게 된다. 즉 L(D,G)=-∞ 는 G의 입장에서 얻을 수 있는 ‘최솟값’이라는 것을 알 수 있다.
  • Vanila GAN Training Algorithm :

    Screen Shot 2021-09-19 at 5.31.37 PM

    • Outer Loop 1번 진행 : 1 iteration(특정 batch size의 mini_batch를 가지고 front&back prop.를 1회 진행)에 DiscriminatorGenerator의 parameter를 update한다
      • Inner Loop : Discriminator update k번 (k는 Hyperparameter) 
      • Generator update 1번

    :question: DiscriminatorGenerator의 parameter가 수렴하기 위해서 iteration을(or Outer Loop) 얼마나 설정해야될까?

    • Training for longer will probably lead to better results but will also take much longer (Pytorch Tutorial)
      # Number of training epochs
      EPOCHS = 5
    

    :question: Algorithm에서 왜 DiscriminatorGenerator보다 더 많이 훈련시키는거지?

    • if the discriminator is already calling generator’s images as real, generator does not have any incentive to do any better (Quora.com)

    :question: 위에 Algorithm에서 Discriminator를 1 iteration마다 한번만(k=1) 훈련시킨 이유 :

    • In most experiments, we don’t find much benefit (or harm) from running the discriminator for multiple steps. Running discriminator truly to convergence would be extremely expensive. (Quora.com)
  • Code(Pytorch) :

    import torch
    import torch.nn as nn
    import torch.optim as optim
    import torchvision
    import torchvision.datasets as datasets
    import torchvision.transforms as transforms
    from torch.utils.data import DataLoader
    from model import Discriminator, Generator, initialize_weights
      
    # Hyperparameters etc.
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    LEARNING_RATE = 2e-4
    BATCH_SIZE = 128
    IMAGE_SIZE = 64
    CHANNEL_IMG = 1
    Z_DIM = 100
    EPOCHS = 5
    FEATURES_disc = 64
    FEATURES_gen = 64
      
    transforms = transforms.Compose(
        [
            transforms.Resize(IMAGE_SIZE),
            transforms.ToTensor(),
            transforms.Normalize(
                [0.5 for _ in range(CHANNEL_IMG)], [0.5 for _ in range(CHANNEL_IMG)]
            )
        ]
    )
      
      
    print("=====================  [STARTING] LOADING DATA =====================")
    dataset = datasets.MNIST(root="dataset/", train=True, transform=transforms, download=True)
    loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=True)
    print("=====================  [FINISHED] LOADING DATA  =====================")
    gen = Generator(Z_DIM, CHANNEL_IMG, FEATURES_gen).to(device)
    disc = Discriminator(CHANNEL_IMG, FEATURES_disc).to(device)
      
    print("=====================  [STARTING] WEIGHT INIT =====================")
    initialize_weights(gen)
    initialize_weights(disc)
    print("=====================  [FINISHED] WEIGHT INIT =====================")
      
    optimizer_gen = optim.Adam(gen.parameters(), lr=LEARNING_RATE, betas=(0.5, 0.999))
    optimizer_disc = optim.Adam(disc.parameters(), lr=LEARNING_RATE, betas=(0.5, 0.999))
    criterion = nn.BCELoss()
      
    fixed_noise = torch.randn(32, Z_DIM, 1, 1).to(device)
      
    gen.train()
    disc.train()
      
    print("=====================  [STARTING] MODEL TRAINING  =====================")
    for epoch in range(EPOCHS):
        for batch_index, (real, _) in enumerate(loader): # _ : we don't need label
            real = real.to(device)
            noise = torch.randn((BATCH_SIZE, Z_DIM, 1, 1)).to(device)
            fake = gen(noise)
      
            # Train Discriminator max log(D(x)) + log(1 - D(G(z)))
            disc_real = disc(real).reshape(-1) # N * 1 * 1 * 1 ==> N
            loss_disc_real = criterion(disc_real, torch.ones_like(disc_real))
            disc_fake = disc(fake).reshape(-1)
            loss_disc_fake = criterion(disc_fake, torch.zeros_like(disc_fake))
            loss_disc = (loss_disc_real + loss_disc_fake) / 2
            disc.zero_grad()
            loss_disc.backward(retain_graph=True)
            optimizer_disc.step()
      
            # Train Generator min log(1 - D(G(z))) ⟷ max log(D(x))
            output = disc(fake).reshape(-1)
            loss_gen = criterion(output, torch.ones_like(output))
            gen.zero_grad()
            loss_gen.backward()
            optimizer_gen.step()
      
    print("=====================  [FINISHED] MODEL TRAINING  =====================")
      
    

추가적인 내용

  • input인 noise(z)는 별 의미 없는 값이 아니라, 이것이 생성될 이미지의 특징을 결정하는 벡터이다. 논문에서는
    • 웃는 여자를 생성한 noise z1
    • 무표정 여자를 생성한 noise z2
    • 무표정 남자를 생성한 noise z3
    • z4 := z1 - z2 + z3이라 할 때, z4를 noise로 쓰면 웃는 남자를 생성해낸다.