0. Abstract
기본적으로 generative model의 성능을 높이기 위해 고안된 architecture이다. 실제와 유사한 이미지를 생성해 주는 generative model을 만들기 위해 generative model을 경쟁시킨다.
Generative model(생성 모델)은 data distribution을 capture 하는 모델이다. Real image를 모방하여 discriminative model이 구별하지 못하도록 한다. Discriminative model(판별 모델)은 생성 모델에서 온 data가 아닌 real image에서 온 데이터의 확률을 측정한다.
- G는 D가 G에서 온 data를 real image data로 착각할 확률을 극대화하는 게 목적이다. -> minimax two-player game
- 함수 G와 D에 unique solution이 있다면 G는 real image를 복원하고 D는 항상 $1/2$의 정답 확률을 갖는다.
- G와 D가 multi-layer perceptron으로 정의된 경우, 전체 시스템은 back-propagation을 통해 학습되고 Markov chains나 unrolled approximate inference networks는 필요하지 않다.
※ 편의상 Generative model은 G, Discriminative model은 D로 정의
1. Introduction
A. Discriminative model의 성공 이유
- Map a high-dimensional, rich sensory input to a class label: Discrimination 목적의 저차원 vector 선별
- Backpropagation and dropout algorithms: Neural network의 문제 극복
- Piecewise linear units: Vanishing gradient를 극복한 ReLu 함수
B. Deep generative model의 한계
- MLE와 관련된 상호작용하는 확률 연산 계산의 어려움
- piecewise linear unit(선형 활성화 함수)들의 이점을 generative context에서는 활용하기 어려움
C. Adversarial process
- 'Adversarial'의 사전적 의미: 대립하는, 적대적인
Adversarial을 사용한 이유는 generative model과 discriminator model이 서로 상반되는 목적을 가지고 있기 때문이다.
지폐위조범(Generator, 생성자)은 경찰을 최대한 속이려고 하고 반대로 경찰(Discriminator, 판별자)은 위조된 지폐를 진짜와 감별하려고(Classify) 노력한다. 이런 경쟁 속에서 두 그룹 모두 속이고 구별하는 서로의 능력이 발전하고 결과적으로 경찰이 진짜 지폐와 위조지폐를 구별할 수 없을 정도(구별할 확률 p=0.5)에 이르게 된다.
이런 GAN의 장점으로는 기존의 deep generative model이 사용하는 approximate inference 기법이나 Markov chains 같은 기술을 사용하지 않아도 된다는 점이 있다. 아래는 GAN의 advasarial process 구조이다.
2. Adversarial Nets
- Uniform distribution으로 input noise를 sampling 진행한다.
- Generator의 input으로 들어갈 noise variable '$z$'의 확률 분포를 $Pz(z)$로 정의한다.
- G는 θg를 파라미터로 가지고 있는 다층 퍼셉트론에 의해 미분 가능한 함수라고 할 때, 데이터 공간을 G($z$;θg)로 매핑해 표현 가능하다.
- 두 번째 다층 퍼셉트론인 Discriminator는 D($x$;θg)로 나타내며 output은 single scalar(스칼라) 값이 나온다.
- $D(x)$는 $x$가 $Pg$(가짜 데이터 분포)가 아닌 진짜 데이터 분포로부터 나올 확률을 나타낸다.
- Discriminator 관점에서는 value function이 maximization 되어야 하고, Generator 관점에서는 value function이 minimization 되어야 한다. 즉, $V(G, D)$에 대한 minimax problem을 푸는 것이다.
A. Value Function: $V(D, G)$
- 첫 번째 항: real data $x$를 discriminator에 넣었을 때 나오는 결과를 $log$ 취했을 때 얻는 기댓값
- 두 번째 항: fake data $z$를 generator에 넣었을 때 나오는 결과를 discriminator에 넣었을 때 그 결과를 $log(1-결과)$ 했을 때 얻는 기댓값
-> 평균을 사용하는 이유? 다수의 데이터를 평균적으로 학습하는 게 real/fake dataset을 모두 고려하게끔 학습을 한다.
B. Discriminator
Real data인 $x$가 들어왔을 때 true=1로 판별하고 ($D(x)=1$), z로부터 생성된 fake data인 $G(z)$가 들어왔을 때 fake=0로 판별한다 ($D(G(z))=0$).
D가 판별을 잘할 때, D가 판별하려는 데이터가 실제 데이터에서 온 샘플일 경우에는 $D(x)$가 1이 되어 첫 번째 항은 0이 되고 $G(z)$가 생성해 낸 가짜 이미지를 구별해 낼 수 있으므로 $D(G(z))$가 1이 되어 두 번째 항은 $log(1-0) = log1 = 0$이 되어 $V(D, G) = 0$이 된다.
즉, D의 이상적인 결과의 최댓값은 ‘0’이다.
C. Generator
Value function $V(D, G)$이 minimization 돼야 하기에 $D(G(z))=1$ 방향으로 학습을 진행한다.
G가 가짜 데이터를 잘 생성해 낸다고 했을 때, 첫 번째 항은 D가 구별해 내는 것에 관한 항으로 G가 고려할 항이 아니고, 두 번째 항은 G가 D를 속일 수 있는 것에 관한 항이라 가정했기 때문에 D가 G가 생성해 낸 이미지를 가짜가 아닌 진짜라고 판단한다. 그러므로 $D(G(z)) = 1$이 되고 $log(1-1) = log0 = -∞$가 된다.
즉, G의 이상적인 결과의 최솟값은 -∞이다.
D. <Figure 1>
- 이미지를 표현할 수 있는 저차원 latent space를 $z$차원으로 설정하고, $z$차원에서 sampling 할 때 사전 확률 분포를 uniform distribution $z$~$Pz(z)$로 설정하면 일정한 간격으로 sampling 되고 generated image가 생성된다.
- 진짜 이미지 데이터 차원 의미의 line을 $x$축이라고 가정하면 $z$차원에서 생성되는 가짜 이미지 데이터도 같은 차원이기에 $z$~$Pz(z)$에서 생성되는 이미지를 $z$에서 $x$축으로 mapping 하는 것으로 표현할 수 있다.
- 이미지의 분포를 표현할 때는 정규분포가 아닌 non-uniform distribution으로 표현된다.
- 의미 있는 이미지들은 $x$축에서 density가 높을 것이고 의미 없는 이미지들은 $x$축에서 expand 하게 분포할 것이다.
(a) 학습 초기에는 real과 fake의 분포가 전혀 다르며 D의 성능이 저조하다.
(b) D가 (a)보다는 안정적으로 real과 fake를 판별해내고 있다.
(c) D의 학습이 어느 정도 이루어지면, G는 실제 데이터의 분포를 모방하며 D가 구별하기 힘든 방향으로 학습한다.
(d) 반복 시 real과 fake의 분포가 거의 비슷해져 구분할 수 없을 만큼 G가 학습되고, G의 성능이 좋아져 결국 D가 이 둘을 구분할 수 없게 되어 1/2의 확률로 계산한다.
E. 학습의 방향성
학습 초반에는 G가 정교하지 않은 fake image를 생성하기 때문에 D가 G가 생성해 낸 데이터와 실제 데이터를 잘 구별한다. $log(1-D(G(z)))$에서 $D(G(z))$ 값이 0이면 gradient(경사) 값이 낮기 때문에 saturation(포화) 된 상태이기에 충분한 gradient가 backpropagation 되지 않아 G의 업데이트에 어려움이 생긴다. 이런 경우에는 $log(1-D(G(z)))$를 minimize 하려고 하는 것보다 $log(D(G(z)))$를 maximize 되게끔 학습하는 것이 더 좋다.
3. Theoretical Results
A. Algorithm 1
"설계한 확률 모델의 최적해를 찾을 수 있는 알고리즘이 존재하는가?"
GAN이 풀어야 하는 problem이 global minimum에서 unique solution을 갖고 어떤 조건을 만족하면 해당 solution으로 수렴하는 지를 증명한다.
G는 $z$~$Pz$일 때 얻는 sample들의 확률분포 $G(z)$로써 $Pg$를 정의한다. $Pdata$의 추정 값으로 수렴하는 것이 목표다. 먼저 Discriminator를 k번 학습시키고 Generator를 1번 학습시키는 과정을 반복한다.
- m개의 노이즈 샘플을 $Pg(z)$로부터 샘플링
- m개의 실제 데이터 샘플을 $Pdata(x)$로부터 샘플링
- 경사상승법을 통해 $V(G, D)$식 전체를 최대화하도록 discriminator parameter 업데이트 ($K$ step)
- $V(G, D)$에서 $log(1-D(G(z)))$를 최소화하도록 경사하강법을 이용해 generator parameter 업데이트 (1 step)
B. Global Optimality of $Pg = Pdata$
"설계한 확률모델이 최적해를 갖는가?"
Generator 관점에서 value function은 $Pg$ = $Pdata$에서 global optimum 값을 가져야 한다. $D*(x)$ = $1/2$ 값을 가진다는 건 discriminator가 가짜 이미지와 진짜 이미지를 판별할 확률이 $1/2$이라는 것이다. 즉, 가짜와 진짜를 구별하지 못하는 상태이다. 이처럼 $V(D, G)$값이 $1/2$에 도달하면 학습을 종료한다.
C. Convergence of Algorithm 1
"설계한 확률 모델이 최적해에 수렴하는가?"
즉, $Pg$가 $Pdata$로 수렴할 수 있는가?
Generative model인 $Pg$ 관점에서 loss함수가 convergence(수렴) 할 수 있는지 여부를 따져야 한다. 이를 위해서는 D를 고정시켰을 때 학습하면서 변화하는 $Pg$에 따라 loss 함수가 convex한지 알아야 한다. $U(Pg, D)$는 $Pg$에 대해 convex 함수이므로, iterative optimization 방식으로 최적해를 찾을 수 있다.
D. Neural network를 generative model에 도입할 경우
GAN을 통해 생성하는 데이터의 분포 $Pg$를 추정하고 최적화하는 것이 아니라 parameter인 θg를 추정하고 최적화한다. Generative model을 multilayer perceptron으로 설정하면 loss function이 convex 하지 않고 multiple critical points를 가질 수 있다. 하지만 theoretical guarantees가 보장되지 않고도 잘 작동할 수 있다.
4. Experiment
- 데이터셋
- MNIST
- Toronto Face Database(TFD)
- CIFAR-10
- Activation function
- Generator nets: mixture of rectifier linear activations and sigmoid activations
- Discrimnator net: maxout activations and only use Dropout
- Noise
- Theoretically apply at intermediate layers of the generator
- Practically use noise as the input to the bottommost layer of the generator network
<Code Practice>
사용한 데이터셋: MNIST
MNIST(Modified National Institute of standards and Technology)는 손글씨 숫자 0~9의 흑백 이미지 데이터셋이다.
- 이미지 크기: 28x28 픽셀의 grayscale(흑백) image
- 채널 수: 1(흑백)
- 데이터 수: 70,000개 (훈련 데이터: 60,000개 / 테스트 데이터: 10,000개)
- 라벨: 각 이미지는 숫자 0~9까지의 숫자 9개로 구성
- 포맷: .gz 형식의 압축 파일로 제공 - https://yann.lecun.com/exdb/mnist/ or Google Colab에서 직접 다운로드 가능
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
# MNIST 데이터 처리에 필요한 패키지
import torchvision
from torchvision import datasets
import torchvision.transforms as transforms
from torchvision.utils import save_image
from torch.autograd import Variable
import os
import torchvision.utils
import matplotlib.pyplot as plt
from IPython.display import Image, display
# GPU 사용 여부 설정
cuda = True if torch.cuda.is_available() else False
if cuda:
generator.cuda()
discriminator.cuda()
adversarial_loss.cuda()
# Preparing Data
# 배치 크기 설정
batch_size = 64
# 이미지 데이터 전처리
transforms_train = transforms.Compose([
transforms.Resize(28), # 이미지를 28x28로 조정
transforms.ToTensor(), # 데이터를 PyTorch 텐서로 변환
transforms.Normalize([0.5], [0.5]) # 픽셀값을 [-1, 1] 범위로 정규화
])
# MNIST 데이터셋 load (훈련 데이터)
train_dataset = datasets.MNIST(root="./dataset", train=True, download=True, transform=transforms_train)
# 데이터로더 생성 (데이터를 배치 단위로 로드)
dataloader = torch.utils.data.DataLoader(train_dataset, batch_size = batch_size, shuffle=True, num_workers=4)
# 첫 번째 batch의 데이터를 로드하여 이미지와 라벨 확인
images, labels = next(iter(dataloader))
img = torchvision.utils.make_grid(images) # 이미지를 격자로 배치
img = img.numpy().transpose(1,2,0) # 이미지를 시각화 가능한 형태로 변환
# 정규화된 이미지를 원래대로 복원
std = [0.5,0.5,0.5]
mean = [0.5,0.5,0.5]
img = img*std+mean
# 배치에 포함된 라벨 출력
print([labels[i] for i in range(64)])
plt.imshow(img) # 이미지 시각화
# 이미지 관련 변수 설정
channels = 1 # 흑백 이미지 -> channel: 1
img_size = 28 # 이미지 크기 28x28
img_shape = (channels, img_size, img_size)
# 잠재 공간(latent space) 차원 설정
latent_dim = 100 # 생성기를 위한 random noise vector 차원
# Generator
# 생성자 클래스 정의
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
def block(input_dim, output_dim, normalize=True):
layers = [nn.Linear(input_dim, output_dim)]
if normalize:
layers.append(nn.BatchNorm1d(output_dim, 0.8)) # 배치 정규화
layers.append(nn.LeakyReLU(0.2, inplace=True)) # 활성화 함수
return layers
# 생성자 모델 정의
self.model = nn.Sequential(
*block(latent_dim, 128, normalize=False),
*block(128, 256),
*block(256, 512),
*block(512, 1024),
nn.Linear(1024, int(np.prod(img_shape))), # 이미지 크기에 맞게 변환
nn.Tanh() # 출력을 [-1, 1] 범위로 변환
)
def forward(self, z):
# z : input noise vector
img = self.model(z)
img = img.view(img.size(0), *img_shape) # 이미지를 적절한 크기로 변환
return img
# Discriminator
# 판별자 클래스 정의
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.model = nn.Sequential(
nn.Linear(int(np.prod(img_shape)), 512), # 입력 이미지 평탄화
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 256),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(256, 1),
nn.Sigmoid(), # 확률값 출력 (실제/가짜 판별)
)
# 이미지에 대한 판별 결과를 반환
def forward(self, img):
img_flat = img.view(img.size(0), -1) # 이미지 평탄화
validity = self.model(img_flat)
return validity
# Loss function & Optimizer
# 학습 관련 하이퍼파라미터
lr = 0.0002 # 학습률
b1 = 0.5 # Adam optimizer beta1 값
b2 = 0.999 # Adam optimizer beta2 값
# 생성자와 판별자 초기화
generator = Generator()
discriminator = Discriminator()
# 손실 함수 정의 (Binary Cross-Entropy Loss)
adversarial_loss = nn.BCELoss()
# Optimizer 정의
optimizer_G = torch.optim.Adam(generator.parameters(), lr=lr, betas=(b1, b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=lr, betas=(b1, b2))
# Training
# 학습 파라미터
n_epochs = 200 # 총 학습 epoch
sample_interval = 2000 # sample 저장 간격
Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor
# 학습 루프
for epoch in range(n_epochs):
for i, (imgs, _) in enumerate(dataloader):
# 진짜와 가짜에 대한 label 정의
real = Variable(Tensor(imgs.size(0), 1).fill_(1.0), requires_grad=False)
fake = Variable(Tensor(imgs.size(0), 1).fill_(0.0), requires_grad=False)
# 실제 이미지를 텐서로 변환
real_imgs = Variable(imgs.type(Tensor))
# 생성자 학습
optimizer_G.zero_grad()
z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], latent_dim))))
generated_imgs = generator(z) # 랜덤 노이즈로 이미지 생성
g_loss = adversarial_loss(discriminator(generated_imgs), real) # 손실 계산
g_loss.backward()
optimizer_G.step()
# 판별자 학습
optimizer_D.zero_grad()
real_loss = adversarial_loss(discriminator(real_imgs), real)
fake_loss = adversarial_loss(discriminator(generated_imgs.detach()), fake)
d_loss = (real_loss + fake_loss) / 2
d_loss.backward()
optimizer_D.step()
# 생성된 샘플 이미지를 저장 및 출력
done = epoch * len(dataloader) + i
if done % sample_interval == 0:
img_path = f"data{epoch}.png"
save_image(generated_imgs.data[:25], img_path, nrow=5, normalize=True)
display(Image(filename=img_path))
# 학습 상태 출력
print(f"[Epoch {epoch}/{n_epochs}] [D loss: {d_loss.item():.6f}] [G loss: {g_loss.item():.6f}]")
1) 생성자
랜덤 노이즈($z$)를 입력받아 가짜 이미지를 생성한다.
- 랜덤 노이즈를 입력으로 사용하여 이미지를 생성
- 생성된 이미지를 판별자에게 전달
- 판별자가 생성 이미지를 진짜 이미지로 분류하도록 속이는 방향으로 학습
- 생성자의 손실(g_loss) 계산: 판별자의 출력과 진짜 label(1) 간의 차이
- 생성자의 가중치 업데이트
2) 판별자
입력 이미지가 진짜 이미지인지 가짜 이미지인지 판별한다.
- 진짜 이미지(MNIST 데이터셋)와 가짜 이미지(생성자가 만든 이미지)를 입력 데이터로 받는다.
- 진짜 이미지에 대해 진짜로 분류 - '1'에 근접한 값 출력
- 가짜 이미지에 대해 가짜로 분류 - '0'에 근접한 값 출력
- 판별자의 손실(d_loss) 계산: 진짜 이미지 손실(real_loss) + 가짜 이미지 손실(fake_loss)
- 판별자의 가중치 업데이트
3) 손실 함수
- BCELoss: 이진 Cross Entropy 손실 함수
- 생성자와 판별자 각각의 손실을 계산하여 업데이트
Result
Epoch | Log | Image |
0 | [Epoch 0/200] [D loss: 0.920350] [G loss: 2.940555] | |
50 | [Epoch 46/200] [D loss: 0.094282] [G loss: 2.612758] [Epoch 47/200] [D loss: 0.155855] [G loss: 1.720819] [Epoch 48/200] [D loss: 0.047181] [G loss: 3.327048] [Epoch 49/200] [D loss: 0.134932] [G loss: 4.085445] [Epoch 50/200] [D loss: 0.063414] [G loss: 4.647917] |
|
99 | [Epoch 95/200] [D loss: 0.158323] [G loss: 3.477714] [Epoch 96/200] [D loss: 0.153723] [G loss: 4.076429] [Epoch 97/200] [D loss: 0.659518] [G loss: 12.104359] [Epoch 98/200] [D loss: 0.069658] [G loss: 2.563983] [Epoch 99/200] [D loss: 0.179063] [G loss: 2.656114] |
|
150 | [Epoch 146/200] [D loss: 0.157671] [G loss: 4.394066] [Epoch 147/200] [D loss: 0.107595] [G loss: 3.803845] [Epoch 148/200] [D loss: 0.171803] [G loss: 3.208174] [Epoch 149/200] [D loss: 0.163514] [G loss: 4.639162] [Epoch 150/200] [D loss: 0.157671] [G loss: 3.665175] |
|
197 | [Epoch 193/200] [D loss: 0.203569] [G loss: 2.818089] [Epoch 194/200] [D loss: 0.294143] [G loss: 3.244365] [Epoch 195/200] [D loss: 0.142358] [G loss: 3.237177] [Epoch 196/200] [D loss: 0.200383] [G loss: 2.998401] [Epoch 197/200] [D loss: 0.110570] [G loss: 2.718446] |
5. Advantages and Disadvantages
A. 장점
- Markov chains는 전혀 필요 없고 gradients를 얻기 위해 back-propagation만 사용된다.
- Generator network가 데이터로부터 직접적으로 업데이트되지 않고 오직 discriminator로부터 오는 gradient만을 이용해 학습이 가능하다.
- 다양한 함수들이 모델에 접목될 수 있다.
- 매우 sharp 하고 degenerate 한 분포도 학습이 가능하다.
B. 단점
- $Pg(x)$가 명시적으로 존재하지 않는다.
- D와 G가 균형을 잘 맞춰 성능이 향상되어야 한다. (이미지 데이터 셋이 많은 그룹만 학습하다가 끝난다면 해당 그룹만 generation을 하는 현상이 발생한다.)
- 최적해 수렴에 있어서 이론적 보장이 부족하다.
6. Conclusions and future work
- CGAN: G, D에 C를 입력 데이터로 추가 시 Conditional generative model로 발전 가능하다.
- Learned approximate inference: 주어진 $x$를 가지고 $z$를 예측하여 수행이 가능하다.
- Another Conditional model: Parameter를 공유하는 conditional model을 학습함으로써 다른 conditional model을 근사적으로 모델링이 가능하다.
- Semi-supervised learning: 라벨링된 데이터가 적을 때, classifiers의 성능 향상이 가능하다.
- Efficiency improvements: G, D를 조정 또는 학습하는 동안 sample $z$에 대한 더 나은 분포를 결정해 학습 가속화 가능하다.
Reference
https://arxiv.org/abs/1406.2661
https://cumulu-s.tistory.com/23
https://happy-jihye.github.io/gan/gan-1/
https://tobigs.gitbook.io/tobigs/deep-learning/computer-vision/gan-generative-adversarial-network
https://89douner.tistory.com/331
https://www.youtube.com/watch?v=odpjk7_tGY0