본문 바로가기

컴린이 탈출기/Vision

Pytorch(파이토치) DCGAN 튜토리얼, 함께 따라해요!

반응형

 


https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html

 

DCGAN Tutorial — PyTorch Tutorials 1.4.0 documentation

Note Click here to download the full example code DCGAN Tutorial Author: Nathan Inkawhich Introduction This tutorial will give an introduction to DCGANs through an example. We will train a generative adversarial network (GAN) to generate new celebrities af

pytorch.org

파이토치 공식 홈페이지의 DCGAN 튜토리얼을 번역한 글입니다!
이해를 잘못해 오역한 부분이나 자연스러운 표현을 위해 의역한 부분이 있을 수 있습니다.
별로 시작하는 문장은 제가 추가한 해석입니다.
잘못된 부분에 대한 지적은 댓글로 부탁드립니다. :)

 


Introduction

이 튜토리얼은 예시를 통해 DCGAN에 대해 소개합니다. 우리는 많은 실제 유명인의 사진을 보여준 후, 새로운 유명인의 사진을 생성할 수 있도록 적대적 생성 신경망(GAN)을 훈련시킬 것입니다. DCGAN 실행을 위한 대부분의 코드는 pytorch/examples에서 가져온 것이며, 이 글은 모델의 구현 방법과 이 모델이 어떻게, 그리고 왜 작동하는지에 대해 설명합니다. GAN에 대한 사전 지식이 요구되지 않으니 걱정하지 않아도 됩니다. 하지만 초보자에게는 실제로 무슨 일이 일어나고 있는지에 대해 고민하는 시간이 필요할 것입니다. 또한, 시간을 위해 GPU를 사용하는 것이 좋습니다. 그럼 처음부터 시작해봅시다!

 

Generative Adversarial Networks

What is a GAN?

GAN은 training 데이터의 분포를 알아내어 같은 분포로부터의 새로운 데이터를 생성할 수 있는 DL 모델을 학습하는 프레임워크입니다. GAN은 Ian Goodfellow에 의해 2014년 만들어졌으며, Generative Adversarial Nets 논문에 처음 실렸습니다. GAN은 Generator(생성자)와 Discriminator(판별자)라는 두 개의 구분되는 모델로 구성이 되어 있습니다. Generator가 하는 일은 training 데이터와 유사한 '가짜' 이미지를 만들어내는 것입니다. Discriminator가 하는 일은 이미지를 본 뒤, 실제 training 데이터인지 generator로부터 생성된 가짜 이미지인지를 출력하는 것입니다. 트레이닝 동안, generator는 끊임없이 더 나은 가짜 이미지를 생성해 disciminator를 능가하려 노력하는 반면, discriminator는 진짜 이미지와 가짜 이미지를 더 잘 감지하고 분류하기 위해 노력합니다. 이 게임의 균형은 generator가 training 데이터에서 꺼내온 듯한 완벽한 가짜 이미지를 만들어 내고 있을 때이고, discriminator는 항상 generator의 결과가 진짜인지 가짜인지 50%의 신뢰도를 가지고 추측하도록 둔다.

Discriminator를 시작으로 튜토리얼을 시작할 텐데, 몇 가지 표기법을 정의해봅시다. 지금부터 'x'를 이미지를 대표하는 데이터라고 부릅시다. D(x)는 x가 generator가 아닌 training 데이터에서 나왔을 확률을 출력하는 discriminator 네트워크이다. 우리는 이미지 데이터를 다루기 때문에 D(x)에 대한 입력의 크기(size)는 3 * 64 * 64입니다. 직관적으로 D(x)는 x가 training 데이터에서 나왔을 때 높고, generator로부터 만들어졌을 때 낮다. D(x)는 전통적인 이진 분류기 (binary classifier)로 생각할 수 있다.

Generator를 위한 표기법으로는,  표준정규분포로부터 추출된 잠재 공간(latent space) 벡터를 'z'라고 하자. G(z)는 잠재 벡터 z를 데이터 공간(data-space)으로 매핑시켜주는 generator 함수이다. G의 목표는 training data의 p_data 분포를 추정하여, 그 추정 분포(p_g)로부터 가짜 샘플을 생성하는 것이다.

따라서, D(G(z))는 Generator의 아웃풋이 진짜 이미지일 확률을 의미한다. (* =가짜 이미지가 진짜 이미지로 판별될 확률) Goodfellow의 논문에 묘사된 것처럼,  D와 G는 D는 진짜와 가짜를 정확하게 구분하는 확률(log D(x))을 키우기 위해, G는 D가 G의 아웃풋을 가짜로 판단할 확률(log(1-D(G(x)))을 낮추는 minmax 게임을 한다.

이론상으로 이 minmax 게임의 해결책은 p_g = p_data일 때이며, discriminator는 인풋이 진짜인지 가짜인지를 랜덤하게 추측한다. 하지만 GAN의 수렴 이론(convergence theory)은 여전히 활발히 연구되는 중이며 실제로 모델들은 항상 이 시점까지 훈련되지 않는다.

 

What is a DCGAN?

DCGAN은 위에서 설명한 GAN의 확장으로, generator와 discriminator에서 각각 Convolution(컨볼루션) 레이어와 Convolutional-transpose 레이어를 사용한다는 점에서 차이가 있다. DCGAN은 Unsupervised Representation Learning with Deep Convolution Generative Adversarial Networks 논문에서 Radford et.al.에 의해 처음 발표되었다. Discriminator는 strided convolution 레이어, Batch Norm 레이어 그리고 Leaky ReLU 활성화 함수로 구성되어 있다. 인풋은 3*64*64 이미지이며 아웃풋은 인풋이 실제 데이터 분포(* = training data)에서 가져온 데이터일 확률입니다. Generator는 convolutional-transpose 레이어, Batch Norm 레이어, 그리고 ReLU 활성화 함수로 구성된다.

+

Convolution-trnsposed layer (전치 합성곱 층) : 파란색이 input, 초록색이 output

 

인풋은 표준정규분포에서 추출된 잠재 벡터 z이고, 아웃풋은 3*64*64 RGB 이미지이다.  Strided cov-transpose 레이어는 잠재 벡터를 이미지와 같은 모양(크기)으로 변환한다. 논문에서 저자는 optimizer를 어떻게 설정하는지, loss function (손실 함수)를 어떻게 계산하는지, model의 weight는 어떻게 초기화하는지 등등 몇 가지 팁을 제시한다. 이 내용들은 뒤에 오는 섹션들에서 설명할 예정이다. 

  Discriminator Generator

구성

Strided Convolution layer

Batch Norm layer

Leaky ReLU

Convolution-transpose layer

Batch Norm layer

ReLU

input 3 * 64 * 64 이미지 잠재 벡터 z
output 진짜(real) 데이터에서 가져온 데이터일 확률 3 * 64 * 64 이미지

 

from __future__ import print_function
#%matplotlib inline
import argparse
import os
import random
import torch
import torch.nn as nn
import torch.nn.parallel
import torch.backends.cudnn as cudnn
import torch.optim as optim
import torch.utils.data
import torchvision.datasets as dset
import torchvision.transforms as transforms
import torchvision.utils as vutils
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from IPython.display import HTML

*본격적인 코드 작성에 앞서 필요한 패키지와 모듈을 import 해줍니다.  

# Set random seed for reproductibility 
manualSeed = 999
print("Random seed:",manualSeed)
random.seed(manualSeed)
torch.manual_seed(manualSeed)

*실행할 때마다 결과가 달라지는 것을 방지하기 위해 시드 번호를 설정해줍니다.

 

Inputs

실행을 위해 몇 가지 input 들을 정의해줍시다.

  • dataroot - 데이터세트 폴더의 루트 경로
  • workers - Dataloader를 이용해 데이터를 로드하기 위해 사용되는 스레드 수
  • batch_size - 학습에서 사용되는 배치 크기. 논문에서는 128.
  • image_size - 학습에서 사용되는 이미지 크기. (모든 이미지가 이 사이즈에 맞게 resize 된다.) 디폴트 값은 64 * 64. 다른 크기로 이용하기 위해서는 D(Discriminator)와 G(Generator)의 구조를 바꾸어야 한다.  자세한 사항은 이 곳에서 볼 수 있다.
  • nc - 인풋 이미지의 색상 채널 수. 컬러 이미지는 이 값이 3이다. (*흑백 이미지의 경우 1)
  • nz - 잠재 벡터의 길이(크기)
  • ngf - Generator를 거치는 피쳐 맵의 크기
  • gdf - Discriminator를 거치는 피쳐 맵의 크기
  • num_epochs - training epoch의 수. 오랜 시간 학습하는 것이 더 좋은 결과를 낳지만, 더 오랜 시간이 걸릴 수 있다.
  • lr - 학습에서 사용되는 learning rate. 논문에서는 0.0002
  • beta1 - Adam optimizer를 위한 하이퍼파라미터이다. 논문에서는 0.5
  • ngpu - 가능한 gpu의 수. 이 값이 0이라면 CPU 모드에서 작동할 것이다. 0보다 더 큰 수일 경우, 숫자만큼의 GPU에서 작동할 것이다.
dataroot = 'data/celeba' #우리 각자의 환경에 맞는 경로를 적어주어야합니다! 

workers = 2

batch_size = 128

image_size = 64

nc = 3

nz = 100

ngf = 64

ndf = 64

num_epochs = 5

lr = 0.0002

beta1 = 0.5

ngpu = 1

 

Data

이 튜토리얼에서 우리는 이 사이트 혹은 구글 드라이브에서 다운 받을 수 있는 Celeb-A Faces dataset을 이용할 것이다. 데이터셋은 img_align_celeba.zip 이라는 파일명으로 다운로드될 것이다. 다운로드한 후, celeba라는 이름의 디렉터리를 만들고 해당 디렉터리에 zip 파일 압축을 해제하자. 그리고, 이 노트북(코드 파일)의 dataroot input을 방금 만든 celeba 디렉터리로 설정하자. 결과적으로 디렉터리 구조는 다음과 같아야 한다.

/path/to/celeba
    -> img_align_celeba
        -> 188242.jpg
        -> 173822.jpg
        -> 284702.jpg
        -> 537394.jpg
           ...

 

우리는 데이터셋의 루트 폴더의 하위 디렉토리인 이미지 폴더 데이터셋을 이용할 것이기 때문에 매우 중요한 단계이다. 이제, 우리는 데이터셋, dataloader를 만들 수 있다. 그리고 실행할 수 있도록 device를 설정할 수 있고 최종적으로 트레이닝 데이터의 일부를 시각화할 수 있다. 

* 무려 사진 20만장에 달하는 거대한 데이터입니다. 저는 Colab을 사용하기 때문에 구글 드라이브에 업로드해서 사용하려고 했으나... 용량 때문인지 구글 드라이브에 업로드를 자꾸 실패해서 (1.3G는 가뿐하잖아 구글아!) 깃에 업로드하고 다운로드하였습니다. :) 꽤나 오랜 시간이 걸린 험난한 과정이었기에 정리를 간단하게 해 보았습니다.  자세한 내용은 아래의 더보기 클릭!

더보기

https://dlsdn73.tistory.com/690

깃 명령어는 아직도 익숙하지가 않아서 위의 사이트를 참고했습니다. 파일용량이 크다 보니 add, commit 하는데도 한참이 걸립니다. 뒤에선 열심히 일을 하고 있기 때문에 인내심을 가지고 기다려줍시다. ^_^

깃 헙 업로드를 마쳤다면 다시 Colab으로 돌아가 봅시다.

!git clone 깃주소

위 명령어를 Colab에서 실행한 뒤, ls 명령어를 실행해보면 clone 한 레포와 같은 이름의 폴더가 생긴 것을 확인할 수 있습니다. 

저는 하루하루 가볍게 공부한 내용을 정리하는 TIL 레포에 데이터를 올렸기 때문에 TIL이라는 폴더가 생긴 것을 확인할 수 있습니다. (같이 clone 되어 온 다른 파일들을 거슬려서(?) 지워주었습니다.) 

dataroot 변수를 다음과 같이 수정해주었습니다.

dataroot = 'TIL/'

 ** TIL/img_align_celeba와 같이 하위 폴더 이름까지 적어줬었는데 파일을 찾을 수 없다는 내용의 Runtime 에러가 발생했습니다. ** 

저와 같은 에러가 발생한 분의 질문을 이 곳에서 찾을 수 있었고, 답변 내용을 참고해 루트를 'TIL/'로 바꾸자 코드가 정상적으로 작동했습니다. 

 

dataset = dset.ImageFolder(root=dataroot,
                           transform= transforms.Compose([
                              transforms.Resize(image_size),
                              transforms.CenterCrop(image_size),
                              transforms.ToTensor(),
                              transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5))                             
                           ]))

dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=workers)

device = torch.device("cuda:0" if (torch.cuda.is_available() and ngpu > 0) else "cpu")

*파이토치에서는 데이터를 쉽게 다룰 수 있게 해주는 모듈들을 제공합니다. 덕분에 간단하게 파라미터 몇 개만으로 이미지를 resizing, centercrop 하고 Normalize 할 수 있습니다. Dataloader는 데이터를 배치 사이즈로 데이터를 쉽게 나누어주는 모듈입니다.

real_batch = next(iter(dataloader))
plt.figure(figsize=(10,10))
plt.axis("off")
plt.title("Training Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=2, normalize=True).cpu(),(1,2,0)))

*데이터 중 일부를 시각화해 나타내 보았습니다.

 

Implementation

우리의 인풋 파라미터와 준비된 데이터를 가지고 이제 본격적인 작업을 시작해봅시다. 우리는 weight initialization(초기화) 전략을 시작으로, generator, discriminator, loss functions, training loop에 대해 자세히 알아볼 것입니다.

Weight Initialization

DCGAN 논문에서 저자는 모든 모델의 weight는 평균 0, 표준편차 0.02의 정규 분포에서 랜덤 하게 초기화되어야 한다고 명시했습니다. 'weights_init' 함수는 초기화된 모델을 인풋으로 가진다. 그리고 모든 convolutional, convolutional-transpose, 그리고 batch-normalization 레이어를 이 기준에 맞게 재초기화한다. 이 함수는 초기화된 직후의 모델에 적용된다.

def weights_init(m):
  classname = m.__class__.__name__

  if classname.find('Conv') != -1:
    nn.init.normal_(m.weight.data, 0.0,0.02)
  
  elif classname.find('BatchNorm') != -1:
    nn.init.normal_(m.weight.data,1.0,0.02)
    nn.init.constant_(m.bias.data,0)

 

Generator

Generator, G는 잠재 공간의 벡터(z)를 데이터 공간으로 매핑하게  디자인되었다. 우리의 데이터는 이미지이기 때문에, z를 데이터 공간으로 변환하는 것은 궁극적으로 학습 데이터와 같은 사이즈(3*64*64)의 RGB 이미지를 만드는 것과 같다. 실제로, 이것은 2d batch norm 레이어와 Relu 활성화 함수와 짝을 이루는 2차원의 strided convolutional transpose layer들을 통해 이루어진다. 인풋 데이터의 범위를 [-1,1]로 조정하기 위해 generator의 아웃풋은 tan h 함수를 거친다. Conv-transpose 레이어 뒤에 Batch norm function이 위치하는 것이 이 논문의 아주 중요한 내용(critical distribution)이다. 이 레이어들은 학습하는 동안에 그래디언트가 흐를 수 있게 돕는다. 논문에서의 generator 이미지는 아래와 같다.

인풋 섹션(nz, ngf 및 nc)에서 인풋에 대해 설정한 것이 generator의 코드 구조에 어떻게 영향을 미치는지 주목해보자. nz는 인풋 벡터 z의 길이(차원)이고, ngf는 generator를 통해 순전파(propagate)되는 피쳐 맵의 사이즈와 관련이 있다. 그리고 nc는 아웃풋 이미지의 채널의 수(RGB 이미지의 경우 3)이다. 아래는 generator에 대한 코드이다.

# Generator

class Generator(nn.Module):
  def __init__(self,ngpu):
    super(Generator, self).__init__()
    self.ngpu = ngpu
    self.main = nn.Sequential(
        # input : z 벡터
        nn.ConvTranspose2d(in_channels=nz,out_channels=ngf*8,kernel_size=4,stride=1,padding=0, bias=False),
        nn.BatchNorm2d(ngf*8),
        nn.ReLU(True),
        # state size. (ngf*8)*4*4
        nn.ConvTranspose2d(ngf*8,ngf*4,4,2,1,bias=False),
        nn.BatchNorm2d(ngf*4),
        nn.ReLU(True),
        # (ngf*4)*8*8
        nn.ConvTranspose2d(ngf*4,ngf*2,4,2,1,bias=False),
        nn.BatchNorm2d(ngf*2),
        nn.ReLU(True),
        # (ngf*2)*16*16
        nn.ConvTranspose2d(ngf*2,ngf,4,2,1,bias=False),
        nn.BatchNorm2d(ngf),
        nn.ReLU(True),
        # ngf * 32 * 32
        nn.ConvTranspose2d(ngf,nc,4,2,1,bias=False),
        nn.Tanh()
        # nc * 64 *64
    )

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

 

이제, 우리는 generator를 인스턴스화하고 weights_init 함수를 적용할 수 있다. Generator 객체가 어떻게 구조화되었는지 확인해보아라.

netG = Generator(ngpu).to(device)

if (device.type=='cuda') and (ngpu>1):
  netG = nn.DataParallel(netG, list(range(ngpu)))

netG.apply(weights_init)

print(netG)

 

Generator

언급한 것과 같이 discriminator, D는 이미지를 인풋으로, 인풋 이미지가 진짜일 확률을 아웃풋으로 하는 이진 분류(binary classification) 네트워크이다. D는 3*64*64 이미지를 입력받아, 여러 개의 Conv2d 레이어, BatchNorm2d 레이어, LeakyReLU 레이어를 지나고, 최종적으로 시그모이드 함수를 거친 확률 값을 아웃풋을 출력한다. 이 문제(태스크)를 위해 필요하다면 더 많은 레이어들로 확장될 수 있지만, strided convolution, Batch Norm, LeakyReLUs를 사용하는 데에도 의미가 있다. DCGAN 논문은 네트워크가 자체적인 풀링 기능을 학습할 수 있다는 점에서 downsample을 위한 풀링보다는 strided convolution을 사용하는 것이 좋다고 이야기한다.  또한 G와 D의 학습과정에서 아주 중요한 그래디언트의 흐름이 batch norm과 leaky relu 함수를 통해 잘 일어날 수 있다고 이야기한다.

# Discriminator

class Discrminator(nn.Module):
  def __init_(self,ngpu):
    super(Discriminator, self).__init__()
    self.ngpu = ngpu
    self.main = nn.Sequential(
        # input : nc * 64 * 64
        nn.Conv2d(nc,ndf,4,2,1,bias=False),
        nn.LeakyReLU(0.2,inplace=True),
        # ndf * 32 * 32
        nn.Conv2d(ndf,ndf*2,4,2,1,bias=False),
        nn.BatchNorm2d(ndf*2),
        nn.LeakyReLU(0.2,inplace=True),
        # (ndf*2) * 16 *16
        nn.Conv2d(ndf*2,ndf*4,4,2,1,bias=False),
        nn.BatchNorm2d(ndf*4),
        nn.LeakyReLU(0.2,inplace=True),
        # (ndf*4)*8*8
        nn.Conv2d(ndf*4,ndf*8,4,2,1,bias=False),
        nn.BatchNorm2d(ndf*8),
        nn.LeakyReLU(0.2,inplace=True),
        # (ndf*8)*4*4
        nn.Conv2d(ndf*8,1,4,1,0,bias=False),
        nn.Sigmoid()
    )

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

 

이제 Generator와 마찬가지로 discriminator를 만들고, weights_init 함수를 적용한 뒤 모델의 구조를 확인할 수 있다.

netD = Discriminator(ngpu).to(device)

if (device.type == 'cuda') and (ngpu > 1):
    netD = nn.DataParallel(netD, list(range(ngpu)))

netD.apply(weights_init)

print(netD)

 

Loss Functions and Optimizers

우리는 D와 G가 loss function과 optimizer를 통해 어떻게 학습할지를 지정할 수 있다. 우리는 파이토치에서 아래와 같은 식으로 정의된 Binary Cross Entropy loss(BCELoss)를 이용할 것이다. 

이 함수가 목표 함수의 두 로그 부분(log(D(x))와 log(1-D(G(z)))를 어떻게 계산하는지 주목하자. 우리는 BCE 방정식의 어떤 부분을 y 인풋과 함께 사용할지 지정할 수 있다.  이 내용은 뒤에 올 트레이닝 루프에서 이루어지지만, y(즉 GT 라벨)을 변해 계산하고자 하는 요소를 어떻게 고를 수 있는지를 이해하는 것이 중요하다. 

다음으로, 우리는 진짜 사진에 대한 라벨을 1, 가짜 사진에 대한 라벨을 0으로 정의하자. 이 라벨은 D와 G의 loss를 계산할 때 이용되며 본래 GAN 논문에서 이용되던 방식이다. 최종적으로, 우리는 D와 G 각각을 위한 두 개의 구분된 optimizer를 둔다. DCGAN 논문에서는 두 optimizer 모두 Adam optimizer이며, learning rate = 0.0002 , Beta1 = 0.5이다. Generator의 학습 과정을 추적하기 위해, 우리는 가우시안 분포에서 온 고정된 배치 사이즈의 잠재 벡터를 생성할 것이다. 학습 과정에서 우리는 주기적으로 이 fixed_noise 벡터를 G에 입력해, 이 벡터로부터 생성된 이미지를 확인할 것이다.

criterion = nn.BCELoss()

fixed_noise = torch.randn(64,nz,1,1,device=device)

real_label = 1
fake_label = 0

optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1,0.999))
optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1,0.999))

 

Training

자! 이제 우리는 GAN 프레임워크의 모든 부분을 정의했고 이제 모델을 훈련시킬 수 있다. GAN을 훈련시키는 것은 일종의 예술 형태임을 명심하라. 잘못된 하이퍼 파라미터 설정은 무엇이 잘못되었는지 설명이 거의 되지 않으채로 모드 붕괴(mode collapse)로 이어지기 때문이다. 여기서는 Goodfellow의 논문에 나오는 알고리즘 1을 거의 따르면서 ganhacks에 나타난 좋은 사례를 따를 것이다.  다시 말해, 우리는 "진짜 이미지와 가짜 이미지에 대한 각기 다른 미니 배치를 구성"할 것이고 또한 G의 목적 함수를 log(D(G(z))를 최대화하는 것으로 조정할 것이다. 학습은 두 개의 메인 파트로 나뉜다. 파트 1은 D를 업데이트하는 것이고, 파트 2는 G를 업데이트하는 것이다.

Part 1 - Train the Discriminator

Discriminator의 목적은 인풋으로 주어진 진짜 혹은 가짜 이미지를 올바르게 분류할 확률을 키우는 것임을 기억해야 한다. Goodfellow의 관점에 따라 우리는 "discriminator를 stochastic gradient를 상승시킴으로써 업데이트하기"를 바란다. 실제로 우리는 log(D(x))+log(1-D(G(z)))를 극대화하고자 한다. Ganhack에서의 제안한 미니 배치로 인해, 우리는 두 단계로 나누어 계산을 할 것입니다. 첫째, 트레이닝 세트로부터 진짜 샘플을 가져와 배치를 구성한 뒤 D를 거치게 해 loss를 계산합니다.(logD(x)) 그리고 거꾸로 그래디언트를 계산합니다.(* = backpropa) 둘째, 우리는 현재의 generator로 만든 가짜 샘플로 배치를 구성한 뒤 D를 거치게 할 것입니다. 그리고 loss를 계산하고(log(1-D(G(z))) 거꾸로 그래이던트를 계산합니다. 이제 모든 진짜 이미지, 가짜 이미지 배치에서의 그래디언트를 모두 합해 discriminator의 opitimizer의 step을 호출합니다. (* step() 함수를 의미하는 것 같습니다. )

Part 2 - Train the Generator

본 논문에서 기술된 것과 같이, 우리는 더 나은 가짜 이미지를 만들기 위한 노력으로  log(1-D(G(z)))를 최소화함으로써 generator를 훈련시키고자 한다. 언급한 것과 같이, 이것은 특히나 학습 초기에 충분한 그래디언트를 제공하지 못한다는 사실이 Goodfellow에 의해 보여졌다. 이를 우리는 log(D(G(z)))를 최대화하는 방향으로 수정하였다. 이것을 다음과 같은 코드를 통해 달성한다 : Generator의 아웃풋을 ~, 진짜 라벨 GT를 이용해 G의 loss를 계산, 거꾸로 G의 그래디언트 계산, 그리고 최종적으로 optimizer step을 이용해 G의 파라미터를 업데이트한다. loss function에 진짜 라벨 GT를 사용하는 것이 직관적이지 않다고 보일 수 있지만, 이것은 우리가 정확히 원하던 BCELoss의 log(x) 파트를 사용할 수 있도록 합니다.

최종적으로, 우리는 몇몇의 통계량을 보고할 것이고, 각각의 epoch이 끝날 때마다 G의 훈련과정을 시각적으로 확인하기 위해 fixed_noise 배치를 generator에 입력할 것입니다. 학습 과정에서 보고될 통계량은 다음과 같습니다.

  • Loss_D - 모든 진짜, 가짜 이미지 배치에 대한 loss를 합한 값
  • Loss_G - log(D(G(z)))로 계산된 generator loss
  • D(x) - 진짜 이미지 배치에 대한 discriminator의 평균 아웃풋. 1에 가깝게 시작해서, G가 나아질수록 이론적으로 0.5에 수렴하게 된다. 
  • D(G(z) - 가짜 이미지 배치에 대한 discriminator의 평균 아웃풋. 첫 번째 값은 D가 업데이트되기 전, 두 번째 값은 D가 업데이트된 후의 숫자이다. 이 숫자들은 0에 가깝게 시작해서 G가 나아질수록 0.5에 가깝게 된다.
# Training

img_list = []
G_losses = []
D_losses = []
iters = 0

print("Starting Training Loop...")

#for each epoch
for epoch in range(num_epochs):
  # for each batch
  for i, data in enumerate(dataloader,0):
    
    # Update D

    # train with all-real batch
    netD.zero_grad()
    real_cpu = data[0].to(device)
    b_size = real_cpu.size(0)
    label = torch.full((b_size,), real_label, device=device)

    output = netD(real_cpu).view(-1)
    errD_real = criterion(output,label)
    errD_real.backward()
    D_x = output.mean().item()

    # train with all-fake batch
    noise = torch.randn(b_size,nz,1,1,device=device)
    fake = netG(noise)
    label.fill_(fake_label)

    output = netD(fake.detach()).view(-1)
    errD_fake = criterion(output,label)
    errD_fake.backward()
    D_G_z1 = output.mean().item()
    errD = errD_real + errD_fake

    optimizerD.step()

    # Update G
    netG.zero_grad()
    label.fill_(real_label)
    
    output = netD(fake).view(-1)
    errG = criterion(output,label)
    errG.backward()
    D_G_z2 = output.mean().item()
    
    optimizerG.step()

    if i % 50 == 0:
      print('[%d/%d][%d/%d]\tLoss_D: %.4f\tLoss_G: %.4f\tD(x): %.4f\tD(G(z)): %.4f / %.4f'
                  % (epoch, num_epochs, i, len(dataloader),
                     errD.item(), errG.item(), D_x, D_G_z1, D_G_z2))
      
      G_losses.append(errG.item())
      D_losses.append(errD.item())

      if (iters % 500 == 0) or ((epoch == num_epochs-1) and (i == len(dataloader)-1)):
            with torch.no_grad():
                fake = netG(fixed_noise).detach().cpu()
            img_list.append(vutils.make_grid(fake, padding=2, normalize=True))

      iters += 1

 

Results

최종적으로, 우리의 결과를 체크해봅시다. 우리는 3가지의 다른 결과를 보게 될 것입니다. 첫 번째는 학습이 되는 동안 D와 G의 loss가 어떻게 변하였는지입니다. 둘째, 우리는 매 epoch마다 고정된 노이즈 벡터 배치에 대한 아웃풋을 시각화할 것입니다. 그리고 세 번째로, 우리는 실제 데이터 배치와 G로부터 생성된 가짜 데이터 배치를 비교해 볼 것입니다. 

Loss versus training iteration

아래는 training iteration 별 D와 G의 loss를 나타낸 그래프입니다.

plt.figure(figsize=(10,5))
plt.title("Generator and Discriminator Loss During Training")
plt.plot(G_losses,label="G")
plt.plot(D_losses,label="D")
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.show()

 

튜토리얼에서 제공한 그래프
직접 코드를 실행했을 때의 결과 ; iteration과 loss의 단위를 확인해주세요! 

 

Visualiztion of G's progression

우리가 어떻게 fixed_noise 배치에 대한 generator의 아웃풋을 매 epoch마다 저장했는지 떠올려보세요. 이제, 우리는 G의 학습 과정을 애니메이션과 함께 볼 수 있습니다. play 버튼을 눌러 애니메이션을 시작해보세요.  (* 그런데 뭐가 문젠지 제 컴퓨터에서는 애니메이션이 작동하지 않네요.. 아쉽 :( )

#%%capture
fig = plt.figure(figsize=(8,8))
plt.axis("off")
ims = [[plt.imshow(np.transpose(i,(1,2,0)), animated=True)] for i in img_list]
ani = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=1000, blit=True)

HTML(ani.to_jshtml())

제가 직접 코드를 실행한 결과입니다.

 

Real Images vs. Fake Images

마지막으로, 진짜 이미지와 가짜 이미지를 옆에 두고 비교해봅시다.

# Grab a batch of real images from the dataloader
real_batch = next(iter(dataloader))

# Plot the real images
plt.figure(figsize=(15,15))
plt.subplot(1,2,1)
plt.axis("off")
plt.title("Real Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=5, normalize=True).cpu(),(1,2,0)))

# Plot the fake images from the last epoch
plt.subplot(1,2,2)
plt.axis("off")
plt.title("Fake Images")
plt.imshow(np.transpose(img_list[-1],(1,2,0)))
plt.show()

튜토리얼에서 제공한 결과입니다.
제가 실행한 결과입니다. 기대에는 못미치지만 epoch 수나 DCGAN이 발표된 년도를 고려했을 때에는 우수한 결과가 아닐까 생각이 듭니다. :)

Where to Go Next

우리의 여행의 끝에 도달했습니다. 하지만 이곳에서 갈 수 있는 곳이 몇 군데 더 있습니다.

  • 더 오랜 시간 학습해 더 좋은 결과가 얻어지는지 확인하기
  • 모델을 수정해 다른 데이터셋을 사용해보고, 이미지의 사이즈와 구조를 바꿔보기
  • 다른 멋진 GAN 프로젝트 찾아보기 here 
  • 음악을 만드는 GAN 만들어보기

 

* 이렇게 GAN 공부 및 실습의 일환으로 Pytorch에서 제공하는 DCGAN 튜토리얼을 번역하고 실습을 진행해보았습니다. 1G에 달하는 대용량의 데이터도 처음 다뤄보았고 이렇게 심하게 팍! 팍! 튀는 Loss 그래프도 처음이라 왜 GAN이 악명 높은 지를 알 수 있었던(?) 시간이었습니다. 논문 본문과 DCGAN에 대해 좀 더 많은 내용을 설명해놓은 괜찮은 자료들을 아래 읽을거리에 소개해두었습니다. DCGAN에 대해 조금 더 알아보고 싶으신 분들은 참고하면 좋을 것 같습니다. 실습 코드 전체는 저의 깃 헙, 바로 이 곳에서 확인할 수 있습니다. 

 

읽을거리

https://arxiv.org/pdf/1511.06434.pdf

라온피플 머신러닝 아카데미 ; DCGAN

Towards Data science ; DCGANs

Towards Data science ; Deeper into DCGANs

반응형