GAN에 대해서 알아보자 - 개념, 알고리즘, 소스코드

최근에 GAN에 관해서 이것저것 많이 찾아볼 기회가 있어서 이것을 정리하고 블로깅하고자 합니다. 이번 블로깅에서는 최대한 수학적인 내용없이 GAN에 대해서 깊게 이해해보고자하는 시간을 가지려고 합니다. 사실 제가 수학을 잘하지도 못하구요...

틀린 내용이 있을 수 있으니 말씀 주시고, 이제 시작해봅니다 ^ㅇ^

오늘의 내용은 크게 3개로 나누어져 있습니다.

  • Generative Model에 관한 내용
  • GAN의 개념
  • GAN의 학습 알고리즘

Introduction

GAN은 일단 생성 모델입니다. 특히 초기 GAN의 연구에서는 랜덤하게 그럴듯한 이미지를 생성하는 것부터 시작했죠. 머신러닝이 생성을한다... 딱 생각했을 때부터 쉽게 상상히 안가는 일입니다. 그만큼 풀고자하는 문제가 어렵다고 볼 수 있습니다.

파인만 아조씨는 What I cannot create, I do not understand. 라는 말을 했습니다. 대우명제로 보자면, 내가 이해했다면 생성할 수 있다고 볼수 있겠죠? ㅎㅎ 아무튼 실제로 그렇게 잘 되지는 않지만, 그만큼 생성한다는 것 자체가 쉽지 않은 영역이라는 것을 말씀드리고 싶었습니다.

무엇인가를 생성한다는 것은 일반적으로 Unsupervised Learning (UL)에 속합니다. 특히 GAN처럼 실제로 있는 것을 생성하는 것이 아니라, 랜덤하게 그럴 듯한 것들을 만드는 것은 그야말로 답이 없는 문제를 푸는 것과 같습니다. 답이 있는 Supervised Learning (SL)에 비해서 조금 더 생각하기 어려운 문제를 풀게 되는 것이죠.

머신 러닝 계열에서 이런 문제를 풀수 있는 대표적인 솔루션으로 크게 3가지를 들 수 있습니다.

  • Boltzmann machine
  • Auto-encoder (AE)
  • Generative Adversarial Network (GAN)

Auto-encoder

모두 다 어려운 영역입니다만, 직관적으로 이해하기 쉬운 AE의 개념에대해서 먼저 간단히 설명하고 넘어가겠습니다. 그림부터 보고 넘어가시죠.

image from https://blog.keras.io/building-autoencoders-in-keras.html

AE는 어려운 UL 문제를 SL 문제로 바꿔서 푼 것이라고 이해하시면 쉽습니다. 데이터 그 자체를 label로 활용한 것이죠.

  • Original Input = $x$
  • Encoder = $f$
  • Compressed representation (latent space) = $z$
  • Decoder = $g$

라고 했을 때, 일반적으로 Auto encoder는 다음과 같이 표현할 수 있습니다.

$$z=f(x), x=g(x) \rightarrow x=g(f(x))$$

즉, Encoder에게는 이미지를 잘 압축하는 방법을, 디코더에게는 잘압축된 표현 방식 (latent space)를 보고 원래 이미지를 재생성해내는 방식을 배우게 하는 것으로 동작하게 됩니다.

Generative Adversarial Network

그렇다면 GAN은 어떨까요? GAN의 기본 구조는 Auto Encder의 뒷부분을 때놓았다고 생각하셔도 좋습니다. 바로 아래와 같이요!

어떤 임의의 표현 $z$ 로부터 이미지를 생성할 수 있는 Decoder, 다른말로 Generator라고 하죠. 이 Generator로부터 이미지를 생성해냅니다.

이 구조만으로는 사실 학습이 불가능하겠죠. 원래 데이터의 분포를 알 수가 없을 테니까요. 여기서 등장하는 것이 바로 Discriminator입니다.

image from https://blog.keras.io/building-autoencoders-in-keras.html

Discriminator는 한국어로는 구별자정도로 해석될 수 있을 것 같습니다. 이 Discriminator는 실제로 구별을 하는 친구이기 때문에 이런 이름이 붙었죠. 정확히 말씀드리자면, 실제 데이터 (Real Data)로부터 데이터를 몇개 뽑고, Generator가 생성한 데이터 몇개를 또 뽑습니다. 그리고 Discriminator에게 이 두개를 섞어서 주죠. Discriminator의 역할은 주어진 데이터가 실제 데이터인지 우리가 생성한데이터인지를 구별시킵니다.

Generator의 주된 목표는 Discriminator가 구분하지 못할 정도로 실제 데이터와 유사한 데이터를 만드는 것입니다. Discriminator의 주된 목표는 Generator가 만든 것과 실제 데이터를 잘 구분해 내는 것이죠.

제설명이 부족한것 같아 GAN의 저자이신 우리의 좋은 동료.. Good fellow 아저씨께서 드신 비유를 한번 살펴 보도록 하겠습니다. 바로, 지폐위조범과 경찰입니다.

image from https://www.slideshare.net/thinkingfactory/pr12-intro-to-gans-jaejun-yoo

가짜 돈을 찍어내는 지폐위조범과, 진짜 돈과 가짜돈을 구별해야하는 경찰의 역활을 생각해볼까요?

  1. 지폐위조범이 가짜돈을 찍어 냅니다.
  2. 그럼 경찰이 이것을 보고 가짜(Fake)인지 진짜(Real)인지 구별합니다. 처음 찍어낸 돈의 품질이 좋지 않을 테니 쉽게 구분할수 있겠군요.
  3. 이를 보고 지폐 위조범은 더 돈을 정교하게 찍어낼 것입니다.
  4. 그러면 또 그것을 보고 경찰이 구별할 것입니다. 조금은더 정교해질 것이니 경찰도 최대한 가짜를 잘 구별하고자 노력할 것입니다.

이것을 계속 반복하다보면 경찰이 지폐위조범이 만든 돈을 잘 구분하지 못하는 순간이 올 것입니다. 즉 진짜 데이터와 가짜 데이터가 구분이 안가는 순간이 오는 것이죠.

그렇습니다! 이제 우리는 드디어 가짜 지폐를 잘 만들어내는 생성자를 가진 것입니다!

이것이 바로 직관적으로 GAN이 동작하는 이유입니다.

GAN의 개념

자 여기까지가 abstract 였고, 쬐끔 더 디테일하게 이해해 보도록 할까요?

Generator

자 세상의 모든 것으로 변할 수 있는 것들이 모여있는 공간을 생각해봅시다. 우리는 이러한 공간을 latent space 라고 부릅니다. 한국어로는 잠재 공간으로 불리고 있습니다. 모든 것으로도 변할 수 있는 것들이 모여 있으니까, 우리가 가지고 있는 실제 데이터 셋으로도 변할 수 있겠죠?

어떻게 이렇게 변화시킬지는 모르겠지만, 어떤 신적인 존재가 그렇게 변화시킨다고 해보도록 할께요. 그림으로 그려보자면 다음과 같은 것입니다.

하지만 신은 언제나 저희 곁에 있지만 실제로 보이지 않습니다 😢 어쩔수가 없군요. 이 신의 역할을 대체할 가짜 신을 만들어 보겠습니다. 바로 저희의 생성자 Generator $G$ 입니다.

자 문제가 있군요. 우리가 $G$ 를 가짜신으로 만드려면 저 신이 어떻게 latent를 우리가 원하는 데이터로 바꾸는지알아야 하는데, latent의 정체도, GOD의 정체도 모릅니다. 😖

하지만, 저희는 그 결과물을 알고 있습니다. 그럼 이렇게 뽑은 결과물들로부터 신이 어떻게 저렇게 멋들어진 데이터를 뽑아낼 수 있었는지에 대해서 추론해볼 수 있을 것입니다.

이런 방법을 바로 sampling 이라고 합니다. 여러번 sampling 함으로서 원래 분포가 어떤지 추론해보는 것이죠.

자 이렇게 뽑아 놓고 보니 서로 생긴 분포들이 매우 다르게 생겼다는 것을 알수 있습니다. 이런 우리의 가짜신 $G$ 가 영 신노릇을 잘 못하는 것 같네요. 🤢

어쩔 수 없습니다. 그럼 이 두 분포를 최대한 똑같게 만들 수 밖에요.

이 두 분포를 똑같이 만든다면, 우리의 $G$ 는 신처럼 실제 데이터를 생성해 낼 수 있을 것입니다!

Sampling

사실 제가 GAN을 이해하는데 있어서 이 Sampling이 잘 와닿지 않았습니다. 야매로 공부해서그런지 이런쪽 머리가 잘 굴러가지가 않네요. 슬픈일입니다.

GAN에서 sampling을 수행할 때, latent space에서 Normal Distribution으로부터 벡터를 몇개 뽑아와 이를 generator에게 던져줍니다.

네? Normal Distribution으로 부터 뽑는다구요?... 라는 생각을 하게 됩니다. 실제 GAN의 코드를 보면 정말로 random을 호출해서 normal distribution으로 값 N개를 뽑게 됩니다. 어떻게 이런 것이 가능한 걸까요?

아래와 같이 이해하시면 그래도 납득이 가게 됩니다. (제 사견입니다.)

저희가 학습 시키는 Generator는 이 latent space로부터 생성된 latent vector를 던져 주면, 어떤한 이미지(어떤 종류라도 상관없지만요)를 생성하도록 교육 됩니다.

사실 초기에는 latent space는 아무런 의미를 가지지 않는 공간이죠. 즉, 정말로 Normal distribution를 따르는 랜덤한 공간입니다. 하지만 학습을 진행하다보면, Generator가 latent space에서 sampling한 latent vector와 특정 이미지들을 연결시키기 시작합니다. 아 latent vector의 첫번째 원소는 이런의미였겠군, 두번째 원소는 저런 의미였겠군 하구요.

실제로 저희가 Generator에게 latent space를 던져주면서 이런 데이터를 만들어야 해! 하고 계속 훈육(?)을 시키다 보면, Generator로는 정말 완전히 자기맘대로 랜덤한 데이터를 마음대로 뱉는 것이 아니라 latent vector에 따라 다른 이미지를 생성하게 됩니다.

사실 사람도, 아무의미없는 것을 몇번 보면 거기에 의미를 부여하곤 하잖아요? 사람들의 징크스 같은 것들처럼요. 인문학적 겜성 1그램 추가하자면, 되게 사람같은 네트워크들이네요. 😉

Discriminator

자 그럼 어떻게 이 두개의 분포를 똑같이 만들어줄수있을까요? 사실 두 분포는 간단한 숫자나 수식이 아닙니다. 매우 복잡하고, 어떻게 생겼는지조차 그리기 어려운 그런 어려운 분포를 따르고 있겠죠 😢 따라서 if문 한줄이나 가벼운 수식 하나로 쉽게 분류할 수는 없을 것 입니다.

그래서 도입한게 바로 구별자 Discriminator 입니다. Discriminator는 두개의 서로 다른 분포가 얼마나 다른지 학습하고, 그 두개의 분포를 명확히 구분할 수 있는 어떤 종류의 함수라도 올수 있습니다! 하지만, 저희는 마법의 머신러닝이 있으니까요. 이 머신러닝 네트워크를 활용할 것입니다.

Discriminator의 주요한 목적은 두개의 분포를 명확히 구분하는 것입니다. 그 구분한 만큼 Generator에게 벌을 줘야하기 때문이죠. 이미 매우 오래전부터 이렇게 두개의 분포를 명확히 구분시킬 수 있는 유명한 Loss가 있죠. 바로 Binary Cross Entropy Loss 입니다. tensorflow나 pytorch에도 이미 구현되어 있는 loss죠? Cross Entropy의 수식은 아래와 같습니다.

$$BCELoss(P,Q)=-(P(x)\cdot\text{log }Q(x) + (1-P(x)) \cdot \text{log }(1 - Q(x)))$$

Cross Entropy에 대해서 조금 더 알고 싶다면 아래 포스팅을 참고해주세요!

 

 

 

Entropy, Cross Entropy, KL-Divergence에 대해 알아보자

Entropy 엔트로피는 머신러닝에서 정보량을 나타내는 방법으로 사용된다. 정보의 량이라는게 되게 추상적으로 들리는데, 생각해보면 되게 간단한 개념이다. ML상에서 굉장히 많이 사용되는 개념

lifeignite.tistory.com

GAN의 Loss는 위의 Loss를 조금 수정해서 사용합니다. 현재는 CE를 이용해서 두 분포의 차이를 구할 수 있고, 그 분포차이를 바탕으로 Generator를 혼내주면서 $G$의 학습을 돕는다고 생각하시면 좋습니다.

그래도, Loss Function이 어떻게 생겼는지는 구경하고 가야겠죠?

GAN의 Loss를 보고 minmax problem이라는 말들을 많이 합니다. 아이 수식자체에 minmax가 들어가 있으니까요. 이 부분을 자세히 집고 넘어가면 너무 글이 길어지고 어려워질 것 같아서 이쯤에서 다음 포스팅으로 넘기도록 하겠습니다.쬐금 귀찮은 증명들이 사아알짝 들어가 있거든요. 이 Loss들을 조금 자세히 이해하시려면 Cross Entropy (CE)에 대해서 조금 자세한 이해를 필요로 합니다!

학습 알고리즘

아래 직접 작성해서 colab으로 실행할 수 있도록 코드를 공개해 두었습니다. 같이 보시면서 코드의 흐름을 파악하시면 좋습니다.

 

choiking10/ML-tutorial

하나하나 구현을 하는게 아니라 한두가지의 시나리오를 가지고, 여러가지 ML 모델을 적용 혹은 개선시키는 작업을 수행해봐야겠다. - choiking10/ML-tutorial

github.com

소스코드를 보고 있다고 가정하고 글을 작성하도록 할께요.

Overview

자 이제 복잡한 내용은 조금 치우고, 어떻게 학습이 이루어지는지 확인하도록 하죠.

Loss에서 Generator의 역할을 $D$ 가 구별할 수 없을 정도로 데이터를 잘 만드는 것입니다. 그리고 Discriminator의 역할은 $G$ 가 만든 것을 완벽히 가짜라고 구별해 내는 것이죠. 이렇게 GAN은 $G$ 가 잘만들면 $D$ 가 혼나고, $D$ 가 잘구별하면 $G$ 가 혼나는 구조로 동작하게 됩니다. 하지만 $G$ 랑 $D$를 모두 생각하기 매우 버겁지 않나요? 사실 이 둘을 모두 한꺼번에 학습시키란 쉽지 않습니다.

그래서 실제 학습을 시킬 때는 두가지 step을 반복하면서 학습을 수행하게 됩니다. 서로 서로가 고정되어있다는 가정하게 업데이트를 진행하게 되는데, 그에 따라서 Loss Function이 살짝 바뀌게 되는 것도 구경하게 되실 겁니다.

어떻게 따로 따로 업데이트할까요? 간단하게 코드 레벨로 보시고 가실께요.

D = Discriminator(image_size, hidden_size).to(device)
G = Generator(latent_size, hidden_size, image_size).to(device)

criterion = nn.BCELoss()
d_optimizer = torch.optim.Adam(D.parameters(), lr=0.0002)
g_optimizer = torch.optim.Adam(G.parameters(), lr=0.0002)

모델이 두개니까, 각 모델의 파라미터를 뽑은 후 서로 다른 optimizer로 업데이트를 수행하는 모습입니다. 어려운 코드는 아니에요!

optimizer의 종류에 대해서는 다음에 한번더 다뤄보도록 할께요!

Step 1. Fix G, Update D

첫번째는 $G$를 멈춰 놓고 $D$를 업데이트 하는 것입니다.

엇...? G를 멈췄다구요?

그러면 G는 변하지 않는다고 생각해도 되겠군요. 그럼 Loss가 이렇게 변할 수 있습니다.

G는 상수니까 골치 아픈 G들을 치환해버렸습니다.

z로부터 뽑는게 아니라 이제 G로부터 뽑는걸로 간단하게 생각해서 뽑으면 되겠군요.

사실 이부분은 소스코드에 영향을 주지 않지만, Loss Function이 이렇게 변할 수 있다는 사실을 이해하고 넘어가면 좋습니다.

소스코드로도 보고 가실께요.

# ---- D ----
z = rand_z()

# real
real_outputs = D(images)
d_loss_real = binary_cross_entropy(real_outputs, real_label)
real_score = real_outputs

# fake
fake_images = G(z)
fake_outputs = D(fake_images)
d_loss_fake = binary_cross_entropy(fake_outputs, fake_label)
fake_score = fake_outputs

# loss
d_loss = d_loss_real + d_loss_fake

# backprop
reset_grad()
d_loss.backward()
d_optimizer.step()

제가 위에서 데이터를 섞어서 D에게 보여준다고 했었죠?

사실 D 입장에서는 섞여서 들어오는지 한꺼번에 들어오는지 번갈아가면서 들어오는지 이런 들어오는 것에 대한 패턴을 알기 어려울 것입니다.

weight를 업데이트하지 않았거든요 아직! 업데이트 하기전까지 넣어준건 한꺼번에 넣어준 것이라고 봐도 무방합니다. (라고 이해했습니다 헷)

# real
real_outputs = D(images)
d_loss_real = binary_cross_entropy(real_outputs, real_label)
real_score = real_outputs

저 images 변수에는 실제 데이터셋으로부터 뽑은 이미지들이 들어있습니다.

실제 데이터셋에서 뽑아서 D가 실제 데이터가 실제 데이터라고 (1이라고) 판단했는지 검사합니다. BCE loss가 쓰인 것을 볼 수 있군요. real_outputs에는 모두 1로된 데이터가 들어가 있습니다. (GT라고 생각하셔도 될것같네요!)

z = rand_z()

# real
# ...

# fake
fake_images = G(z)
fake_outputs = D(fake_images)
d_loss_fake = binary_cross_entropy(fake_outputs, fake_label)
fake_score = fake_outputs

latent space에서 latent vector z를 뽑고 G에게 넘겨줍니다. 그럼 G는 fake images들을 생성할 것이고, 이를 D에 넘겨주죠. 수식으로 보자면 $D(G(z))$ 에 해당하겠군요. BCE loss가 똑같이 쓰인 것을 볼 수 있군요. fake_outputs에는 모두 0로된 데이터가 들어가 있습니다.

# loss
d_loss = d_loss_real + d_loss_fake

# backprop
reset_grad()
d_loss.backward()
d_optimizer.step()

전체 Loss는 fake일때와 real일때의 loss값을 d_loss에 더해주고 backward를 수행해줍니다. 아직 모델의 파라미터에는 업데이트가 이루어지지 않았습니다!

그리고 d_optimizer.step() 을 함으로서 discriminator의 파라미터만 업데이트가 되었군요.

Step 2. Fix D, Update G

자 마찬가지로 D를 멈춰두고 G를 업데이트 해 봅시다. Step 1처럼 Loss 에 변화가 있을 거에요.

뭔가 커다란 것이 없어 졌습니다. 바로 $\mathbb{E}{x \sim P{\text {data }}(x)}[\log D(x)]$ 자체가 날아가버렸네요. Gradient를 계산하는 행위는 미분과정을 통해 이루어집니다. 그리고 고등학교 때 달달외웠던 미분의 가장 기본적인 성질은 상수는 날아가버린다는 거죠.

그리고 우리는 D를 고정했고, G가 없는 모든 항들은 상수 취급됩니다. 그러니까 굳이 계산할 필요가 없다는 거죠! 코드를 Step1 보다는 조금더 간단하게 작성할 수 있겠네요.

# ---- G ----
z = rand_z()
fake_images = G(z)
outputs = D(fake_images)
g_loss = binary_cross_entropy(outputs, real_label)

reset_grad()
g_loss.backward()
g_optimizer.step()

첫번째 항이 날아가버렸으므로, real data 자체를 D에 넣을 필요가 없어집니다. 그 외에는 Step 1과 같게 되겠습니다.

역시 눈에 띄는건 g_optimizer를 통해서 generator의 gradient만 업데이트 해주었다는 사실입니다.

마무리

한줄 정리

latent space에서 sampling 한 z로부터 Generator는 어떠한 데이터 분포를 만듭니다. 그리고 이렇게 생성한 Generator의 분포와 실제 데이터 분포를 Discriminator로부터 비교하게 함으로서 G가 잘생성하면 D를 혼내주고, D가 잘 구분하면 G를 혼내주는 방법으로 학습을 수행합니다.

사족

휴, 드디어 벼루고 벼뤘던 GAN의 시작을 끊게되었네요.

GAN은 크게 4부작으로 작성될 예정입니다.

다음 글 링크는 아래에 있습니다. 

 

 

[논문정리] GAN에 대해서 알아보자 (2) - 증명과 한계 그리고 WGAN의 등장

자 다시 GAN 포스팅을 작성할 시간이네요. 아 GAN은 쓸게 많아서 너무 괴로워요... PS쪽이 확실히 더 작성하기 편하네요. 익숙한 개념이라서 그런걸까요? 오늘 알아볼 내용은 GAN의 증명과 한계 그리

lifeignite.tistory.com

하나하나가 정말 작성하기 매우매우 귀찮군요.

다음부터는 주제를 좀 쓰기 쉬운걸로 작성해야겠어요. ㅎㅎ...

Reference

+ Recent posts