Gorio Tech Blog search

GAN(Generative Adversarial Networks), GAN 논문 설명

|

목차


이 글에서는 2014년 6월 Ian J. Goodfellow 등이 발표한 Generative Adversarial Networks(GAN, 생성적 적대신경망)를 살펴보도록 한다.

간단히 GAN은 두 가지 모델을 동시에 학습시키는 구조이다. G(Generator, 생성자)라는 모델은 직접 볼 수 없는 진짜 데이터와 최대한 비슷하게 생긴 가짜 데이터를 만드려고 하고, D(Distriminator, 식별자 또는 감별자)라는 모델은 자신에게 주어진 데이터가 진짜 데이터인지 가짜 데이터인지 최대한 구분하려고 한다.

GAN을 도식화한 구조는 다음과 같다. 출처

논문에서는 설명을 위한 예시로 화폐 위조범($G$)와 경찰($D$)을 제시하였다. 다만 차이가 있다면,

  • 위조범은 진짜를 볼 수 없다는 것(그래서 장님blind라 불린다)
  • 경찰은 자신이 판별한 결과를 위조범에게 알려준다 는 것이 있다.

참고: $G$로 들어가는 입력 벡터를 뜻하는 noise는 latent variable이라고도 하며, Auto-encoder에서 출력 영상을 만들기 위한 source와 비슷하기에 이 표현도 사용된다.
또 GAN은 특정한 모델 구조를 가진 것이 아니므로 코드가 특별히 정해진 것은 아니다.

논문을 적절히 번역 및 요약하는 것으로 시작한다. 많은 부분을 생략할 예정이므로 전체가 궁금하면 원 논문을 찾아 읽어보면 된다.


Generative Adversarial Networks(GAN)

논문 링크: Generative Adversarial Networks

초록(Abstract)

이 논문에서는 적대적으로 동작하는 두 생성 모델을 동시에 학습시키는 새 framework를 제안한다. 생성자 G는 원본 data distribution을 흉내내려 하고, D는 눈앞의 데이터가 G에게서 온 것인지를 판별한다. G의 목적은 D가 최대한 실수하게 만드는 것이고, D는 당연히 최대한 정확하게 진짜/가짜를 판별하는 것이다.
이는 2인 minimax 게임과 비슷하다. 어떤 유일한 해가 존재하여 최종적으로 D는 실수할 확률이 0.5가 된다(즉 찍는 수준).
G와 D가 multi-layer perceptron으로 구성되면 전체 시스템은 backpropagation으로 학습될 수 있다.
GAN에는 어느 과정에서든 마르코프 체인이나 기타 다른 네트워크가 필요가 전혀 없다.


서론(Introduction)

적대적인 두 네트워크를 학습시킨다. D는 원본 data distribution인지 G에서 온 것인지를 판별하고, G는 D가 실수하도록 가짜 데이터를 잘 만들어내는 것이 목표이다.
이 framework는 많은 특별한 학습 알고리즘과 optimizer를 사용할 수 있다. 앞서 말한 대로 multi-layer perception을 쓰면 다른 복잡한 네트워크는 필요 없이 오직 forward/backpropagation만으로 (이 논문에서는 dropout을 또 쓴다) 학습이 가능하다.


관련 연구(Related Works)

궁금하면 읽어보자.

  • RBMs: restricted Boltzmann machines, 잠재 변수를 가진 유향 그래프 모델에 대한 대안으로, 무향 그래프 모델
  • DBMs: deep Boltzmann machines, RBMs와 비슷함. 다양한 변형이 존재
  • MCMC: Markov chain Monte Carlo methods, 위 모델의 측정 방법
  • DBNs: Deep belief networks, 하나의 무향 레이어와 여러 유향 레이어의 hybrid 모델. 계삭적 문제가 있음
  • NCE: noise-contrasive estimation, log-likelihood를 근사하거나 경계값을 구하지 않는 방법
  • GSN: generative stochastic network, 확률분포를 명시적으로 정의하지 않고 분포 샘플을 생성하도록 학습시키는 방법을 사용
  • adversarial nets: 적대적 망은 생성 중 feedback loop를 필요로 하지 않아 sampling에서 Markov chain이 필요가 없다. 이는 backpropagation 성능 향상으로 이어진다.
  • auto-encoding varitional Bayes와 stochastic backpropagation은 생성 머신을 학습시키는 방법들 중 하나이다.

적대적 망(Adversarial nets)

기호 설명
$x$ 데이터
$p_g$ $x$에 대한 생성자의 분포
$p_z(z)$ input noise 변수
$\theta_g$ multilayer perceptrions의 parameters
$G$ $\theta_g$에 의해 표현되는 미분가능한 함수
$G(z; \theta_g$) data space에 대한 mapping
$D(x)$ $x$가 $p_g$가 아니라 원본 데이터에서 나왔을 확률
$D(x; \theta_d)$ 두 번째 multilayer perceptron

D의 목적은 데이터가 ‘원본’인지 ‘G가 생성한 데이터’인지 판별하는 것이므로 어떤 데이터에 대해 정확한 label(‘원본’ 또는 ‘G로부터’)을 붙이는 것이다. G의 목적은 D가 실수하게 만드는 것, 즉 어떤 데이터가 주어졌을 때 D가 ‘원본’이라고 판별할 확률과 ‘G로부터 나온 데이터’라고 판별할 확률을 모두 높이는 것(정확히는 같게)이다.
즉 $log(1-D(G(z)))$를 최소화하도록 G를 훈련시킨다.

D와 G 모두에 대해 value function $V(G, D)$를 정의하면,

\[min_G max_D V(D, G) = \mathbb{E}_{x \sim p_{data}(x)}[log D(x)] + \mathbb{E}_{x \sim p_{z}(z)}[log (1-D(G(z)))]\]

위 식의 의미는,

  • $min_G$: G는 V를 최소화하려고 한다.
  • $max_D$: D는 V를 최대화하려고 한다. 2-player minimax 게임과 같으므로 당연하다.
  • $\mathbb{E}$: 기댓값
  • $x \sim p_{data}(x)$: $x$가 원본 데이터 분포에서 왔을 때

D가 아주 똑똑한 경찰이라면, $x$가 실제로 원본에서 온 것이라면 $D(x)=1$이 될 것이고, $G(z)$에서 온 것이라면 $D(G(z))=0$이 된다. 만약 G가 완벽한 위조범이 되었다면, $D(x) = {1 \over 2}$이다.
따라서 D의 입장에서 V의 최댓값은 0이 되며, G의 입장에서 V의 최솟값은 $-\infty$임을 알 수 있다.

학습시킬 때, inner loop에서 D를 최적화하는 것은 매우 많은 계산을 필요로 하고 유한한 데이터셋에서는 overfitting을 초래하기 때문에, $k$ step만큼 D를 최적화하고 G는 1 step만 최적화하도록 한다.
학습 초반에는 G가 형편없기 때문에 D는 진짜인지 G가 생성한 것인지를 아주 잘 구분해 낸다.
또 G가 $log(1-D(G(z)))$를 최소화하도록 하는 것보다는 $log(D(G(z)))$를 최대화하도록 하는 것이 더 학습이 잘 된다. 이는 G가 형편없을 때는 $log(1-D(G(z)))$의 gradient를 계산했을 때 너무 작은 값이 나와 학습이 느리기 때문이라고 한다.

파란 점선은 disctiminative distribution(D), 검정색은 원본 데이터($p_x$), 초록색은 생성된 분포$p_g$(G), $x$는 원본 데이터 분포를, 화살표는 $x=G(z)$ mapping을 나타낸다. (a) 초기 상태. (b) D 학습 후, (c) G 학습 후, 분포가 비슷해지는 것을 볼 수 있다. (d) 여러 번의 학습 끝에 G가 완전히 원본을 흉내낼 수 있는 경지에 도달함. 즉 $p_g = p_{data}$. D는 이제 진짜인지 가짜인지 구분할 수 없다. 즉 $D(x) = {1 \over 2}$.


이론적 결과(Theoretical Results)

수학을 좋아한다면 직접 읽어보자.

  • Algorithm 1
    • for epochs do
      • for k steps do
        • noise prior $p_g(z)$로부터 $m$개의 noise sample $z^{(1)}, …, z^{(m)}$을 뽑는다.
        • noise prior $p_{data}(x)$로부터 $m$개의 noise sample $x^{(1)}, …, x^{(m)}$을 뽑는다.
        • D를 다음 stochastic gradient로 update한다. (ascending)
          • $ \nabla_{\theta_d} {1 \over m} \sum^m_{i=1} [log D(x^{(i)}) + log (1-D(G(z^{(i)})))] $
      • noise prior $p_g(z)$로부터 $m$개의 noise sample $z^{(1)}, …, z^{(m)}$을 뽑는다.
      • G를 다음 stochastic gradient로 update한다. (descending)
        • $ \nabla_{\theta_d} {1 \over m} \sum^m_{i=1} [log (1-D(G(z^{(i)})))] $
  • 이 minimax 게임은 $p_g = p_{data}$에 대한 global optimum을 가진다.
    • G를 고정했을 때, optimal한 D는 다음과 같다.
\[D^*_G(x) = {p_{data}(x) \over p_{data}(x) + p_g(x)}\]
  • Algorithm 1은 수렴한다.

실험(Experiments)

MNIST, Toronto Face Database(TFD), CIFAR-10에 대해 학습을 진행했다.

  • G는 rectifier linear activations와 sigmoid를 사용했고, D는 maxout activations를 사용했다.
  • Dropout은 D를 학습시킬 때 사용했다.
  • noise는 G에서 가장 밑의 레이어에만 input으로 넣었다.

자세한 실험 조건은 직접 읽어보자.

가장 오른쪽 열은 바로 옆에 있는 생성된 이미지와 가장 비슷한 학습 샘플이다. a) MNIST b) TFD c) CIFAR-10(fully connected model) d) CIFAR-10(convolutional D와 “deconvolutional” G)

숫자 간 보간을 했을 때는 위와 같이 된다. 물론 GAN을 통해 생성한 것이다.


장단점(Advantages and disadvantages)

단점

  • $p_g(x)$가 명시적으로 존재하지 않는다.
  • D는 G와 균형을 잘 맞추어서 성능이 향상되어야 한다(G는 D가 발전하기 전 너무 발전하면 안 된다).

장점

  • 마르코프 체인이 전혀 필요 없이 backprop만으로 학습이 된다.
  • 특별히 어떤 추론(inference)도 필요 없다.
  • 다양한 함수들이 모델에 접목될 수 있다.
  • 마르코프 체인을 썼을 때에 비해 훨씬 선명한(sharp) 이미지를 결과로 얻을 수 있다.

결론 및 추후 연구(Conclusions and future work)

  1. conditional generative model로 발전시킬 수 있다(CGAN).
  2. Learned approximate inference는 $x$가 주어졌을 때 $z$를 예측하는 보조 네트워크를 학습함으로써 수행될 수 있다.
  3. parameters를 공유하는 조건부 모델을 학습함으로써 다른 조건부 모델을 대략 모델링 할 수 있다. 특히, deterministic MP-DBM의 stochastic extension의 구현에 대부분의 네트워크를 쓸 수 있다.
  4. Semi-supervised learning에도 활용 가능하다. classifier의 성능 향상을 꾀할 수 있다.
  5. 효율성 개선: G와 D를 조정하는 더 나은 방법이나 학습하는 동안 sample $z$에 대한 더 나은 distributions을 결정하는 등의 방법으로 속도를 높일 수 있다.

참고문헌(References)

논문 참조!


보충 설명

목적함수

D의 목적함수는 G를 고정한 채로 진짜 데이터 $m$개와 가짜 데이터 $m$개를 D에 넣고, G에 대한 V를 계산한 뒤 gradient를 구하고 V를 높여 D를 최종적으로 업데이트한다.

\[max_D V(D) = {1 \over m } \sum^m_{i=1} log D(x^i) + {1 \over m } \sum^m_{i=1} log D(1 - D(G(z^i)))\]

G의 목적함수는 D를 고정한 채로 가짜 데이터 $m$개를 생성해 V을 계산한 뒤, G에 대한 V의 gradient를 계산하고 V를 낮춰 G를 업데이트한다.
G의 목적함수는 gradient가 0에 가까워지는 것을 막기 위해 논문에서 언급된 팁을 반영한 것이다.

\[min_G V(G) = {1 \over m} \sum^m_{j=1} log(D(G(z^j)))\]

목적함수 최적화의 의미

Machine Learning 관점에서 보면 모델이 loss가 최소화되는 parameter를 찾아가는 과정이다.
또는 진짜 데이터의 분포와 G가 생성한 가짜 데이터 분포 사이의 차이를 줄이는 것과도 같다.

수학적으로는 D가 이미 최적이라는 가정 하에, GAN이 목적함수를 최적화한다는 과정($p_{data}$와 $p_g$를 똑같이 만드려는 것)은 $p_{data}$와 $p_g$ 사이의 Jensen-Shannon divergence(JSD)를 최소화하는 것과 같다.
JSD는 Kullback–Leibler divergence의 대칭(symmetrized and smoothed) 버전이다. 그래서 GAN은 KLD를 최소화하는 것이라고 말하기도 한다.

분포 $P$와 $Q$에 대해, $KLD = D(P \Vert Q), M = {1 \over 2}(P+Q)$라 할 때, JSD는

\[JSD(P \Vert Q) = {1 \over 2} D(P \Vert M) + {1 \over 2} D(Q \Vert M)\]

이다.


학습 방법

GAN은 서로 경쟁하는 두 가지 모델을 학습시킨다. GAN을 쓰려면 다음 방법을 따른다.

  1. 우선 다음을 정의한다.
    1. R(Real): 실제 데이터. 논문에선 $x$로 표시
    2. I(Input 또는 Imaginary): G가 가짜 데이터를 생성할 source. 논문에선 $z$로 표시.
      • $G(z)$는 $G$가 $z$를 입력으로 받아 생성한 가짜 데이터이다.
    3. $G$(generator): 생성자, 위조범
    4. $D$(Distriminator): 감별자 또는 식별자, 경찰
  2. 다음 전체 과정을 num_epochs 동안 반복한다:
    1. D를 training하는 과정(d_steps만큼 반복): D와 G를 모두 사용은 하지만 D의 parameter만 업데이트한다.
      1. $D$에 실제 데이터($x$)와 정답(1)을 입력으로 주고 loss를 계산한다.
      2. $D$에 가짜 데이터($G(z)$)와 정답(0)을 입력으로 주고 loss를 계산한다.
      3. 두 loss를 합친 후 $D$의 parameter를 업데이트한다.
    2. G를 training하는 과정(g_steps만큼 반복): D와 G를 모두 사용은 하지만 G의 parameter만 업데이트한다.
      1. $D$에 가짜 데이터($G(z)$)와 정답(1)을 입력으로 주고 loss를 계산한다.
      2. 계산한 loss를 이용하여 $G$의 parameter를 업데이트한다.

단점 및 극복방안

GAN 논문에서는 수학적인 증명이 포함되어 있지만(최소 해를 가지며, 충분히 학습할 시 항상 그 해답을 찾는다), 여러 요인들로 인해 실제 학습시킬 때에는 학습이 좀 불안정하다는 단점이 있다.

Mode Collapsing

간단히 이 현상은 학습 모델이 실제 데이터의 분포를 정확히 따라가지 못하고 그저 뭉뚱그리기만 하면서 다양성을 잃어버리는 것이다.
예를 들면 1~9까지의 숫자 9개를 만드는 대신 5만 9개 만드는 것과 비슷하며, MNIST의 경우 10종류의 모든 숫자가 아닌 특정 숫자들만 생성하는 경우이다.

이는 GAN이 단순히 목적함수의 loss만을 줄이려는 방향으로 설정되어 있어 생기는 현상이다. 이 현상은 GAN의 개선 모델들에서 대부분 해결된다.

Oscillation

G와 D가 수렴하지 않고 진동하는 모양새를 보일 때가 있다. 이 역시 비슷한 이유로 발생하며, 나중 모델들에서 해결된다.

G와 D 사이의 Imbalance

학습을 진행하면 처음에는 D가 발전하고 나중에 G가 급격히 학습되는 형상을 보이는데, 처음부터 D가 너무 성능이 좋아져버리면 오히려 G가 학습이 잘 되지 않는 문제가 발생한다(D가 시작부터 G의 기를 죽이는 셈).

해결방안

  • 진짜 데이터와 가짜 데이터 간 Least Square Error를 목적함수에 추가한다(LSGAN).
  • 모델의 구조를 convolution으로 바꾼다(DCGAN)
  • mini-batch별로 학습을 진행할 경우 이전 학습이 잘 잊혀지는 것을 막기 위해 이를 기억하는 방향으로 학습시킨다.

튜토리얼

50줄로 짜보는 튜토리얼

원문 링크는 여기, 번역본은 여기에서 볼 수 있다.
해당 튜토리얼에서는

  1. 이 전체 과정을 num_epochs(여기서는 5000)만큼 반복한다.
    1. training D(d_steps만큼 반복):
      1. 가우시안 분포를 따르는 데이터를 Real Data로 생성하고
      2. 그 momentum(mean, std, skews, kurtoses)를 계산하여 D에게 전달, error를 계산한다.
      3. 또 Fake data를 G가 생성하게 하고
      4. D가 error를 계산하게 한다.
      5. 위 두 과정(1~2, 3~4)으로 D의 parameter를 업데이트한다.
    2. training G(g_steps만큼 반복):
      1. G로 Fake data를 생성한다.
      2. D에게서 판별 결과를 받아온다.
      3. G가 error를 계산하게 한다.
      4. G의 parameter를 업데이트한다.

코드는 원문에도 소개되어 있지만 전체는 사실 186줄이다(…) 물론 GAN의 핵심 코드는 50줄 정도이다.

MNIST 튜토리얼

GAN의 핵심 부분을 제외한 부분은 여기를 참고하면 된다.

우선 기본 설정부터 하자.

import torch
import torch.nn as nn
from torch.optim import Adam
from torch.utils.data import DataLoader
from torchvision import datasets, transforms

import argparse

from matplotlib import pyplot as plt
import numpy as np

import pickle
import os
import imageio


parser = argparse.ArgumentParser(description='GAN tutorial: MNIST')

parser.add_argument('--epochs', type=int, default=100, help='number of epochs')
parser.add_argument('--batch-size', type=int, default=64, help='size of mini-batch')
parser.add_argument('--noise-size', type=int, default=100, help='size of random noise vector')
parser.add_argument('--use-cuda', type=bool, default=True, help='use cuda if available')
parser.add_argument('--learning-rate', '-lr', type=float, default=0.0002, help='learning rate of AdamOptimizer')
parser.add_argument('--beta1', type=float, default=0.5, help='parameter beta1 of AdamOptimizer')
parser.add_argument('--beta2', type=float, default=0.999, help='parameter beta2 of AdamOptimizer')
parser.add_argument('--output-dir', type=str, default='output/', help='directory path of output')
parser.add_argument('--log-file', type=str, default='log.txt', help='filename of logging')

args = parser.parse_args()

os.makedirs(args.output_dir, exist_ok=True)

use_cuda = args.use_cuda and torch.cuda.is_available()

transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean=(0.5,), std=(0.5,))
])

mnist = datasets.MNIST(root='data', download=True, transform=transform)
dataloader = DataLoader(mnist, batch_size=args.batch_size, shuffle=True)

Generator는 다음과 같이 선언한다. 레이어는 총 4개, activation function은 LeakyRELU와 Tanh를 사용하였다.

class Generator(nn.Module):
    
    def __init__(self):
        super(Generator, self).__init__()
        self.linear1 = nn.Linear(in_features=100, out_features=256)
        self.linear2 = nn.Linear(in_features=256, out_features=512)
        self.linear3 = nn.Linear(in_features=512, out_features=1024)
        self.linear4 = nn.Linear(in_features=1024, out_features=28 ** 2)
    
    
    def forward(self, x):
        """
        :param x: input tensor[batch_size * noise_size]
        :return: output tensor[batch_size * 1 * 28 * 28]
        """
        x = nn.LeakyReLU(0.2)(self.linear1(x))
        x = nn.LeakyReLU(0.2)(self.linear2(x))
        x = nn.LeakyReLU(0.2)(self.linear3(x))
        x = nn.Tanh()(self.linear4(x))
        return x.view(-1, 1, 28, 28)

Discriminator는 다음과 같다. Linear Layer는 G의 역방향으로 가는 것과 비슷하지만, activation function에는 차이가 있다.

class Discriminator(nn.Module):
    
    def __init__(self):
        super(Discriminator, self).__init__()
        self.linear1 = nn.Linear(in_features=28 ** 2, out_features=1024)
        self.linear2 = nn.Linear(in_features=1024, out_features=512)
        self.linear3 = nn.Linear(in_features=512, out_features=256)
        self.linear4 = nn.Linear(in_features=256, out_features=1)
    
    
    def forward(self, x):
        """
        :param x: input tensor[batch_size * 1 * 28 * 28]
        :return: possibility of that the image is real data
        """
        x = x.view(-1, 28 ** 2)
        x = nn.LeakyReLU(0.2)(self.linear1(x))
        x = nn.Dropout()(x)
        x = nn.LeakyReLU(0.2)(self.linear2(x))
        x = nn.Dropout()(x)
        x = nn.LeakyReLU(0.2)(self.linear3(x))
        x = nn.Dropout()(x)
        return nn.Sigmoid()(self.linear4(x))

GAN의 핵심 부분은 다음과 같다. 위의 Gaussian 분포 예제와 크게 다르지 않아서 크게 설명은 필요없을 듯 하다. 차이점을 조금 적어보자면

  1. 분포의 momentum을 G의 데이터 생성 source로 사용하는 대신 길이 100(MNIST의 경우 보통)짜리 random vector를 사용한다. G는 이 길이 100짜리 벡터를 갖고 MNIST의 숫자 이미지와 비슷한 이미지를 생성하려고 하게 된다.
  2. .cuda()DataLoader를 사용하는 것 정도가 있겠으나 GAN의 핵심 부분은 아니다.
    for epoch in range(args.epochs):
        for D_real_data, _ in dataloader:
            
            batch_size = D_real_data.size(0)
            
            # Training D with real data
            D.zero_grad()
            
            target_real = torch.ones(batch_size, 1)
            target_fake = torch.zeros(batch_size, 1)
            
            if use_cuda:
                D_real_data, target_real, target_fake = \
                D_real_data.cuda(), target_real.cuda(), target_fake.cuda()
            
            D_real_decision = D(D_real_data)
            D_real_loss = criterion(D_real_decision, target_real)
            
            # Training D with fake data
            
            z = torch.randn((batch_size, args.noise_size))
            if use_cuda: z = z.cuda()
            
            D_fake_data = G(z)
            D_fake_decision = D(D_fake_data)
            D_fake_loss = criterion(D_fake_decision, target_fake)
            
            D_loss = D_real_loss + D_fake_loss
            D_loss.backward()
            
            D_optimizer.step()
            
            # Training G based on D's decision
            G.zero_grad()
            
            z = torch.randn((batch_size, args.noise_size))
            if use_cuda: z = z.cuda()
            
            D_fake_data = G(z)
            D_fake_decision = D(D_fake_data)
            G_loss = criterion(D_fake_decision, target_real)
            G_loss.backward()
            
            G_optimizer.step()

전체 코드는 여기를 참조하라.


이후 연구들

사실 2014년 발표된 original GAN은

  • 학습이 불안정하고
  • 고해상도 이미지는 생성하지 못하는 한계를 갖고 있었다. 논문에서 optimal point가 있고 그쪽으로 수렴한다는 것을 보였지만, 실제로는 여러 변수 때문에 학습이 항상 잘 되는 것이 아니라는 현상을 보인다. 이러한 문제를 보완하기 위해 GAN 이후로 수많은 발전된 GAN이 연구되어 발표되었다.

그 중에서 가장 중요한 것을 3가지 정도만 뽑자면

  1. Convolution을 사용하여 GAN의 학습 불안정성을 많이 개선시킨 DCGAN(Deep Convolutional GAN, 2015)
  2. 단순 생성이 목적이 아닌 원하는 형태의 이미지를 생성시킬 수 있게 하는 시초인 CGAN(Conditional GAN, 2014)
  3. GAN이 임의의 divergence를 사용하는 경우에 대해 local convergence함을 보여주고 그에 대해 실제 작동하는 GAN을 보여준 f-GAN(2016)

일 듯 하다.

많은 GAN들(catGAN, Semi-supervised GAN, LSGAN, WGAN, WGAN_GP, DRAGAN, EBGAN, BEGAN, ACGAN, infoGAN 등)에 대한 설명은 여기에서, DCGAN에 대해서는 다음 글에서 진행하도록 하겠다.