1. Chapter 5 - GAN 개념
1.1) 이미지 생성하기
Backquery(역절의)
일반적인 Classifier를 생각해보자.
Output Layer의 Activation Function으로 Softmax Activation을 사용하는 Multi-Class Classification Task를 수행한다.
예를 들어 MNIST 분류기는 784(28 * 28)개의 값을 Input으로 받아 10개의 Output으로 변환하기에, 처음보다 정보량이 감소한다.
만약 네트워크를 반대로 뒤집으면 이는 정보량을 감소시키는 행위가 아니라, 조금 더 사이즈가 큰 데이터(28 * 28)로 변환하는 것이다.
즉, 레이블을 주면 Neural Network를 통해 그에 해당하는 이미지를 생성해주는 것이다.
하나의 숫자(Class)를 표현하는 One-Hot Encoding된 Vector를 Input으로 하는 Pre-trained된 Network에 넣어서 해당 숫자에 맞는 이상적인 이미지를 거꾸로 만들어낼 수 있다.
이를 BackQuery(역질의)라 부른다.
Backquery를 통해 만들어진 이미지는 다음과 같은 특징을 지닌다.
1. 동일한 One-Hot Encoding Vector면 같은 결과를 출력한다.
2. 이 때 출력되는 결과 이미지는 해당 이미지의 Class에 대응되는 모든 훈련 데이터의 평균적인 이미지이다.
다만 우리는 네트워크 모델을 이용하여 만들고 싶은 이상적인 결과는 다음과 같다.
1. 각자 서로 다른 이미지를 생성한다.
2. 평균처럼 애매모호한 이미지가 아니라 훈련 샘플처럼 보이는 이미지를 생성한다.
위의 두 가지 목적을 달성하는 것은 사실적이고 그럴듯한 이미지 생성에 굉장히 중요하다.
다만 일반적인 Backquery로는 이 목적을 달성하지 못하기에, 다른 네트워크 구조가 필요하다.
1.2) 적대적 훈련
GAN은 총 두 개의 신경망이 있다. Generator(생성기)와 Discriminator(판별기)라는 이름으로 불린다.
Discriminator
- Input Image(주어진 이미지 데이터)가 Real인지, Fake인지 판별하는 역할
- Input Image가 Real이면 1을 반환
- Input Image가 Fake(Generator가 생성한 Image)이면 0을 반환
Generator
- Discriminator가 Generate된 Image들을 Input으로 받았을 때 Real이라고 판별하도록 만드는 역할
- Discriminator를 무사히 속이면 Generator에 Reward를 준다
- Discriminator를 속이는데 실패하면 (Fake로 판별) Generator에 Penalty를 부여한다
만약 Generator의 성능이 별로 좋지 않다면 Discriminator는 굉장히 쉽게 이미지들을 분류할 수 있다.
하지만 Generator를 점차 훈련시키다 보면 점차 진짜와 구별이 잘 안되는 이미지를 생성해낼 것이다.
즉, 훈련이 지속될수록 Discriminator의 성능이 좋아지지만 이와 동시에 Generator 또한 Reward & Penalty에 의해 훈련이 되어 성능이 증가할 것이다.
궁극적으로 Generator는 원본 이미지와 잘 분간이 가지 않는 이미지들을 생성할 것이다.
이처럼 Discriminator와 Generator는 서로 적대적 관계로 경쟁을 하게 되며, 서로를 뛰어넘기 위해 노력하기 때문에 결과적으로는 둘 다 성능이 좋아지게 된다. 이를 Generative Adversial Network(GAN) 이라 한다.
이는 굉장히 Novel한 접근법인데, 단순히 경쟁을 통해 발전한다는 아이디어도 중요하지만 Real Image를 판별하기 위해 어떤 방식을 따르고 어떠한 Loss Function을 거쳐야 하는지 등을 하나하나 세세하게 설정하지 않아도 된다는 점에서 의의가 있다.
1.3) GAN 훈련
GAN은 Discriminator과 Generator를 모두 훈련한다.
이때, 두 모델 중 하나만 먼저 훈련하고 같은 데이터로 다시 한번 다른 모델을 훈련하는 것은 하지 말아야 한다.
제대로 훈련하기 위해서는 두 모델을 동시에 훈련시키면서 양쪽 모두 비슷한 수준으로 훈련이 이루어지도록 유도해야 한다.
실제 훈련 과정은 다음과 같이 3단계로 이루어진다.
1. Discriminator에 Real Image를 보여주고, 이 때 1.0을 반환하도록 학습한다.
2. Discriminator에 Fake Image를 보여주고, 이 때 0.0을 반환하도록 학습한다.
3. Generator에 Generator가 만든 Fake Image를 Discriminator가 Real Image로 인식하여 1.0을 반환하도록 만들도록 학습한다.
1단계 Training
- Discriminator에 Real Image를 보여준 후 Image들을 Classify 하며 DIscriminator를 학습시킨다
- 결과는 1.0이 나오도록 해야하며, 오차가 발생하면 이를 Discriminator를 update하는데 사용한다
2단계 Training
- Discriminator에 Generator가 만든 Fake Image를 보여주며 Discriminator를 학습시킨다
- 결과는 0.0이 나오도록 해야하며, 오차를 통해 Discriminator만을 update 한다 (Generator는 update하지 않는다)
3단계 Training
- Generator가 만든 Fake Image에 대해 Discriminator가 이를 1.0을 반환하도록 Generator를 학습시킨다
- 즉 Generator가 Discriminator를 속여 Real Image처럼 분류하도록 만드는 것이다
- 오차를 통해 Generator만을 update 한다 (Discriminator를 잘못 분류하도록 학습시킬 수 없으므로 update X)
1.4) 훈련하기 어려운 GAN
실전에서는 GAN을 훈련하는게 굉장히 까다로울 수 있다.
Discriminator와 Generator가 서로 적대적인 관계로 발전하게 했을 때, 두 개의 성능이 서로 균형있게 잘 맞춰져 있어야 GAN이 제대로 훈련된다.
Discriminator의 성능이 너무 빨리 좋아져버리면, Generator가 이를 따라잡지 못할 수도 있다.
Discriminator가 너무 늦게 훈련된다면, Generator는 별로 어렵지도 않은 문제인, 조악한 이미지를 가려내면서 신나게 점수를 획득할 것이다.
1.5) 정리
분류: 데이터를 감소시키는 것에서 출발한다.
- 신경망을 통해 분류를 한다는 것은, 입력 값을 줄여 Class당 하나의 출력값을 가지게끔 만드는 행위이다
생성: 데이터를 확장하여 얻어내는 것이다.
- Generative Neural Network은 작은 수의 Seed를 훨씬 큰 크기를 가지는 출력값으로 확장하는 역할을 한다
GAN(Generative Adversial Network): 두 개의 신경망 모델을 포함한다.
- Discriminator: Train Dataset을 Real Image로 판별하고, Generator에서 생성된 Image들을 Fake Image로 판별한다
- Generator: Discriminator가 Generator에서 생성된 Fake Image를 Real로 착각하도록 속일 수 있는 Data를 생성한다
- GAN 이 어떤 식으로 훈련을 하고 어떤 경우에 실패하는지에 대해서는 아직 연구가 성숙하지는 않았다
GAN Training Process는 3가지 단계로 이루어진다
- 1) Discriminator가 Train Dataset을 Real Image로 인식하도록 학습
- 2) Discriminator가 Generator가 생성한 Image를 Fake Image로 인식하도록 학습
- 3) Generator가 생성한 Fake Image를 Discriminator가 Real Image로 인식하도록 속이는 학습
2. Chapter 6 - 단순한 1010 패턴
실제 이미지를 GAN으로 생성하기 전 간단한 '1010 패턴' 형식의 값을 생성하는 GAN을 먼저 구현해보자.
실제 데이터(Real, Training Dataset)는 항상 '1010 패턴'을 반환하는 함수로 대체했다.
PyTorch의 pytorch.utils.data.Dataset Class는 이렇게 간단한 구조에서는 일단 필요하지 않다.
Generator는 결과에서 4개의 값을 출력하는 신경망으로, 훈련 후에는 '1010 패턴'을 생성해야 할 것이다.
DIscriminator는 4개의 값을 받아서 데이터가 실제 데이터 Source에서 온 것인지, Generator에서 온 것인지를 판단해야 한다.
import torch
import torch.nn as nn
import pandas
import matplotlib.pyplot as plt
import random
import numpy
2.1) 실제 데이터 소스
실제 데이터에 대하여 1010 패턴을 변환하는 함수는 다음과 같이 만들 수 있다.
def generate_real():
real_data = torch.FloatTensor([1, 0, 1, 0])
return real_data
다만 실제 세계의 데이터는 정확히 딱 떨어지는 값일 확률이 거의 없으므로 약간의 임의성을 추가하여 함수를 실제 상황과 비슷하게 만들자.
4개의 값을 가진 tensor를 반환하는 함수를 이용해서, 첫 번째와 세 번째 수는 0.8과 1.0 사이의 임의의 값(1)을 가지게 했고
두 번째와 네 번째 수는 0.0과 0.2 사이의 임의의 값(0)을 가지도록 했다.
def generate_real():
real_data = torch.FloatTensor(
[
np.random.uniform(0.8, 1.0),
np.random.uniform(0.0, 0.2),
np.random.uniform(0.8, 1.0),
np.random.uniform(0.0, 0.2),
]
)
return real_data
generate_real()
tensor([0.9047, 0.0550, 0.9836, 0.1680])
이 설정은 특히 Apple Silicon 기반의 Mac에서 MPS를 활용하여 GPU와 유사한 가속을 제공받고자 할 때 유용하다.
MPS는 Apple Silicon(M1, M1 Pro, M1 Max 등)을 탑재한 Mac에서 PyTorch 연산을 가속화하기 위해 사용할 수 있는 기술이다.
device = torch.device("mps:0" if torch.backends.mps.is_available() else "cpu")
2.2) 판별기 만들기
일반적인 PyTorch의 Model 방식과 비슷하게 진행한다. nn.Module을 상속받아서 forward( )를 구현하는 일반적인 방식을 사용한다.
Q1. Binary Classification에서 (각 숫자가 0 or 1인지 구분) Output Layer의 FC Layer output_features 수가 왜 1인가, 2여도 되지 않나?
A. nn.Linear(input_features, 1)이 맞다.
나중에 Sigmoid Activation을 Output Layer Activation으로 적용해야 Binary Classification이 되는데,
Sigmoid 함수에 하나의 값(out_features=1)을 넣어야 해당 값을 기준으로 0.5가 넘으면 1, 아니면 0을 반환하기 때문이다.
forward( )함수는 Model의 위쪽에서 input을 받아 Feed Forward를 통해 output을 내보내는 역할을 한다.
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
# 신경망 Layer 정의
self.model = nn.Sequential(
nn.Linear(4, 3),
nn.Sigmoid(),
nn.Linear(3, 1),
# output activation 사용: loss_function은 BCEWithLogitLoss() 사용불가, Just BCELoss or MSELoss
nn.Sigmoid(),
)
# 손실함수 설정
self.loss_function = nn.MSELoss()
# SGD Optimizer 설정
self.optimizer = optim.SGD(self.parameters(), lr=1e-2)
# 진행 측정을 위한 변수 초기화
self.counter = 0
self.progress = []
pass
def forward(self, x):
# 모델 실행
return self.model(x)
def train(self, inputs, targets):
# mps:0 사용
inputs, targets = inputs.to(device), targets.to(device)
# 신경망 출력 계산
outputs = self.forward(inputs)
# loss 계산
loss = self.loss_function(outputs, targets)
# counter를 증가시키고 10회마다 오차 저장
self.counter += 1
if self.counter % 10 == 0:
self.progress.append(loss.item())
pass
if self.counter % 10000 == 0:
print(f"counter = {self.counter}")
pass
# gradient를 초기화하고 backpropagation후 weight update
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
def plot_progress(self):
df = pd.DataFrame(self.progress, columns=["loss"])
df.plot(
ylim=(0, 1.0),
figsize=(16, 8),
alpha=0.1,
marker=".",
grid=True,
yticks=(0, 0.25, 0.5),
)
pass
2.3) 판별기 테스트하기
아직 Generator를 만들지 않았기 때문에 Discriminator가 Generator와 제대로 경쟁하는지 제대로 테스트하기에는 어렵다.
여기서 해볼 수 있는 확인 절차는 Discriminator가 임의의 데이터에 대하여 진짜를 구별할 수 있느냐에 대한 부분이다.
이 테스트를 통해 Discriminator가 적어도 쓸모없는 노이즈로부터 실제 데이터를 골라낼 정도의 성능을 가졌는지의 여부를 확인할 수 있다.
만약 이것조차 제대로 동작하지 않는다면, 좀 더 어려운 작업인 그럴듯한 가짜로부터 진짜를 구별해내는 작업은 시도조차 못 할 것이다.
따라서 해당 테스트는 처음부터 가능성이 없는 모델들을 골라내는 역할을 한다.
def generate_random(size):
random_data = torch.rand(size)
return random_data
이번에 만든 함수가 더 일반적이다. tensor 크기를 매개변수로 받기에 더 유연한 구조를 갖는다.
D = Discriminator().to(device)
epochs = int(1e+4)
for epoch in range(epochs):
# real data train
D.train(generate_real(), torch.FloatTensor([1.0]))
# fake data train
D.train(generate_random(4), torch.FloatTensor([0.0]))
pass
generate_real( )에서 얻은 실제 데이터를 대상으로 훈련을 한다. 목표 출력은 1.0의 값을 가진 tensor이다.
즉, 모델로 하여금 '1010 패턴'을 감지했을 때 결과로 1.0을 내놓도록 유도한다.
이와 마찬가지로 generate_random( )을 통해 그럴듯한 가짜 이미지도 제시하고, '1010 패턴'이 아닌 데이터라고 판단할 때 목표 출력값은 0.0이고, 예측도 0.0의 결과를 내도록 유도한다.
실제 훈련은 M1 Mac Air로 mps:0으로 MPS(Metal Performance Shaders) device를 사용하여 PyTorch 연산을 가속화했을 때 1m 14s 정도의 시간이 걸렸다.
D.plot_progress()
판별기가 훈련을 지속함에 따라 1010 패턴에 적응하고, 이는 점차 성능이 나아지기에 Loss가 0을 향해 나아간다.
이제 훈련이 완료된 판별기에 1010 패턴에 맞는 값을 집어넣으면 결과는 1.0에 가까운 값일 것이며, 임의의 패턴에 맞는 값을 집어넣으면 결과는 0.0에 가까운 값을 반환할 것이다.
print(D.forward(generate_real().to(device)).item())
print(D.forward(generate_random(4).to(device)).item())
0.7875930666923523
0.15186366438865662
지금까지는 GAN 구현의 3단계 중 하나도 하지 않았다. 우리는 아직 Discriminator가 Generator와 경쟁을 하면서 잘 작동하는지는 증명을 하지 않았다.
즉 Discriminator가 실제 Training Data인 Real Data에 대해 1.0을 반환하도록 훈련시킨 것에 불과하다.
즉, 실제 데이터에서 나오는 패턴과 Noise를 섞은 것에서 나오는 패턴을 넣었을 때 결과가 다른지 확인하는 것이었다. 만약 이 작업조차 할 수 없다면 이보다 더 어려운 작업은 당연히 할 수 없다.
Q2. Discriminator과 마찬가지로 Generator를 생성한 객체 D, G의 Train 함수 안에 inputs, targets 모두 .to(device)를 통해 MPS device로 옮겨줬는데 왜 forward( )시 오류가 날까?
A. G.forward( ) or D.forward( )를 사용할 때의 forward( )를 호출할 때는 입력 데이터를 device로 옮기지 않았기 때문에 그렇다.
train method 내에서 inputs, targets 를 디바이스로 옮기는 코드는 train Method를 통해 학습할 때만 적용되고,
forward Method를 직접 사용할 때는 적용되지 않기 때문이다.
따라서, forward( ) Method를 사용할 때는 반드시 x를 .to(device)를 통해 MPS device로 옮겨줘야 한다.
2.4) 생성기 만들기
Generator는 Discriminator를 속일 수 있어야 하므로 현재 예제에서는 Output Layer의 output_features=4가 되어 실제 데이터와 일치해야 하므로 총 4개의 노드가 필요하다.
그렇다면 Hidden Layer의 Feature(Node)수는 얼마나 커야 할까? Input Layer의 Feature수는?
사실 정해진 것은 없다.
훈련을 위해 필요하다면 충분히 커도 되지만, 너무 과도하게 크면 훈련 시간이 길어진다는 문제가 발생한다.
또한 Discriminator가 전반적으로 잘 배울 수 잇는 정도의 적당한 성능이어야 한다.
이러한 이유로 많은 연구자들은 Discriminator를 복사하여 Generator를 생성하는 작업을 시작하고는 한다.
따라서 Input Layer의 Feature 수는 1개, Hidden Layer의 Feature 수는 3개, Output Layer의 Feature 수는 4개이다.
Generator의 Layer 구조는 Discriminator와 반대이다. 따라서 Discriminator class의 코드를 복사해서 수정한다.
Generator의 Input 값은 어떻게 해야할까?
일단은 제일 간단하게 상수 하나를 대입해보자. 굉장히 큰 값은 훈련을 힘들게 할 수도 있으니 적당한 값인 0.5부터 시작해보자.
한계에 다다르면 추후에 값을 살짝 바꾸면 된다.
Generator의 경우에는 self.loss가 없다. 이 과정이 사실상 필요 없기 때문이다.
GAN 훈련 반복문을 생각해보면 Loss Function은 오직 Discriminator의 결과에만 적용이 된다.
Generator는 Discriminator로부터 흘러온 Gradient 오차를 통해 update가 된다.
즉, Generator의 class 안에서: Loss는 Discriminator꺼, Optimizer는 Generator(본인)꺼를 사용한다.
class Generator(nn.Module):
def __init__(self):
# PyTorch Parent Class 초기화
super(Generator, self).__init__()
self.model = nn.Sequential(
nn.Linear(1, 3), nn.Sigmoid(), nn.Linear(3, 4), nn.Sigmoid()
)
# SGD optimizer 설정
self.optimizer = optim.SGD(self.parameters(), lr=1e-2)
# 진행 측정을 위한 변수 초기화
self.counter = 0
self.progress = []
pass
def forward(self, x):
# model 실행
return self.model(x)
def train(self, D, inputs, targets):
inputs, targets = inputs.to(device), targets.to(device)
# 신경망 출력 계산
g_output = self.forward(inputs)
# 판별기로 전달
d_output = D.forward(g_output)
# 오차 계산
loss = D.loss_function(d_output, targets)
# counter를 증가시키고 10회마다 오차 저장
self.counter += 1
if self.counter % 10 == 0:
self.progress.append(loss.item())
pass
# 기울기를 초기화하고 역전파 후 가중치 갱신
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
pass
def plot_progress(self):
df = pd.DataFrame(self.progress, columns=["loss"])
df.plot(
ylim=(0, 1.0),
figsize=(16, 8),
alpha=0.1,
marker=".",
grid=True,
yticks=(0, 0.25, 0.5),
)
pass
inputs(입력값)는 Generator의 신경망에 self.forward(inputs)를 통해 전달된다.
그리고 Generator의 출력인 g_output은 D.forward(g_output)을 통해 Discriminator의 신경망에 전달되며 이에 따른 분류 결과인 d_output이 나온다.
손실은 d_output과 정답지(원하는 목표값) 간의 차이로 계산된다. loss = D.loss_function(d_output, targets)
이 손실로부터 loss가 backpropagation되며, 이는 Discriminator에서 계산 그래프를 통해 Generator로 전달된다.
이 update는 D.optimizer가 아닌 self.optimizer를 통해 전달된다.
즉, 이 방법으로 GAN 훈련 과정의 3단계에서 의도한 것과 같이 Generator의 가중치만 update 한다.
이때, 여기서 사용되는 target 값은 Generator가 Discriminator로 하여금 믿게 하고 싶은 value이다.
Q3. 복잡한 Discriminator 객체 자체를 train( ) 함수에 전달해도 되나?
A. Python에서는 객체를 전달할 때 객체를 단순 복사하는 것이 아니라 객체의 참조를 전달하기 때문에 문제가 없다.
즉. Generator 코드가 오차 기울기를 역전파하기 위해 객체를 바꿔도 문제가 없다.
현재 Generator의 train( )함수 안에는 진행 상황을 체크하는 출력함수도 없앤 상태이다.
Discriminator의 train( )함수 안에 같은 역할을 하는 코드가 있고, 판별기의 함수는 실제 데이터만 진행에 반영하기 때문에 이 정보가 더 중요하기 때문이다.
2.5) 생성기 결과 확인하기
Generator 훈련을 본격적으로 하기 전에, 올바른 결과가 나오는지 살짝 확인해보자.
새로운 셀에서 새로운 Generator 객체를 하나 만들어서 0.5라는 단일한 값을 가지는 tensor를 한 번 넘겨보자.
G = Generator()
G.forward(torch.FloatTensor([0.5]))
tensor([0.4686, 0.5358, 0.4336, 0.5195], grad_fn=<SigmoidBackward0>)
물론 아직 Generator가 훈련이 되지 않았기 때문에 '1010 패턴'의 형식은 아니지만 우리가 원했던 4자리 digit의 결과가 나왔다.
2.6) GAN 훈련하기
이제 GAN을 총 3단계에 거쳐 훈련시킬 준비가 되었다. 다음과 같이 코드를 작성하자.
%%time
# New Discriminator & Generator 생성
D = Discriminator().to(device)
G = Generator().to(device)
image_list = []
# Discriminator & Generator 훈련
epochs = int(1e4)
for epoch in range(epochs):
# 1단계: Discriminator가 Real Data에 대해 1.0을 반환하도록 훈련
D.train(generate_real(), torch.FloatTensor([1.0]))
# 2단계: Discriminator가 Generator가 생성한 Fake Data에 대해 0.0을 반환하도록 훈련
# Generator의 Gradient가 계산되지 않도록 detach() 함수를 이용
D.train(
G.forward(torch.FloatTensor([0.5]).to(device)).detach(),
torch.FloatTensor([0.0]),
)
# 3단계: Generator가 생성한 Fake Data에 대해 Discriminator가 1.0을 반환하도록 훈련
G.train(D, torch.FloatTensor([0.5]), torch.FloatTensor([1.0]))
# 1000 epoch 마다 image add
if epoch % 1000 == 0:
image_list.append(G.forward(torch.FloatTensor([0.5]).to(device)).detach().cpu().numpy())
pass
일단 새로운 Discriminator와 Generator 객체를 생성한다.
1단계에서는 Discriminator가 Real Data에 대해 1.0을 반환하도록 훈련한다.
2단계에서는 Discriminator가 Generator에서 생성한 Fake Data(G.forward(torch.FloatTensor([0.5]).detach())에 대해 0.0을 반환하도록 훈련한다.
Q4. detach는 어떠한 역할을 하는가?
A. Generator의 output에 적용되어 계산 그래프에서 Generator를 떼어내는 역할을 한다.
일반적으로 backward( )를 Discriminator의 loss에 호출하는 행위는 gradient loss를 계산 그래프의 전 과정에 걸쳐서 계산하라는 의미이다. 이는 Discriminator의 loss부터, Discriminator 그 자체, 그리고 Generator까지 전해진다.
현재는 Discriminator만 학습하기 때문에 Generator의 기울기까지 계산해야 할 이유가 없다. 따라서 detach( )는 Generator의 출력에 적용이 되며, 계산 그래프의 특정 부분에서 연산이 끝나도록 한다.
3단계에서는 Generator를 훈련하고 Generator의 입력값은 0.5로 설정한 후 Discriminator로 ouput을 전달하는 단계이다.
이번에는 오차가 Discriminator로부터 Generator까지 전해져야 하므로 detach를 쓰지 않는다.
Generator의 train 함수는 Generator의 가중치만 update할 뿐이므로, Generator의 가중치만 업데이트하는 3단계에서는 Discriminator에 특별한 작업을 가해줄 필요가 없다.
Q5. GAN의 training process의 2, 3단계에서 각각 Discriminator만, Generator만 훈련시키는 이유가 무엇인가?
A. 2단계에서 DIscriminator만 훈련하는 이유는 큰 네트워크 구조에서는 필요 없는 부분에 대해 계산을 하지 않아야 효율적이고 빠르게 결과를 얻을 수 있기 때문이다.
3단계에서 Generator만 훈련하는 이유는 Discriminator가 잘못된 Fake Data에 대해 학습하는 것을 방지해야 하기 때문이다.
코드의 맨 윗 부분에 %%time을 넣어 시간이 전체적으로 얼마나 걸리는지 확인하자.
counter = 10000
counter = 20000
CPU times: user 1min 29s, sys: 9.74 s, total: 1min 39s
Wall time: 2min 15s
이후 Discriminator의 Loss를 D.plot_progress( )를 통해 훈련이 어떻게 진행되었는지 확인해보자.
D.plot_progress()
위의 그래프를 살펴보면 Discriminator의 Loss가 0.25 부분인 것을 확인할 수 있다.
이전에는 Loss가 0을 향해 떨어졌다면, 이번에는 Loss가 0.25 근처에서 진동하고 있다.
이는 Discriminator가 Real Data와 Fake Data를 잘 판별하지 못하는 결과를 내놓는다면, 그 결과로 0.0이나 1.0이라는 확실한 결과가 아니라 애매한 0. 5라는 결과를 내놓을 것이다. MSELoss를 사용했기 때문에 0.5를 제곱한 0.25가 손실로 나오는 것이다.
결과를 보면 훈련이 진행되는 중간에 Loss가 조금 떨어지긴 했다.
이는 Discriminator의 성능의 향상을 뜻하는 것인가?
1010 패턴을 인식한 건지, 무엇인가 발전이 있긴 했는지, 훈련이 잘 이루어져서 판별을 잘하게 된 상태인지 확실치가 않다.
훈련의 후반부로 갈수록 Loss는 다시 0.25로 튀어오른다. 이건 매우 좋은 징조이다.
Generator가 다시 DIscriminator가 판별하기 쉽지 않은 데이터를 만들어내기 시작했다는 뜻으로 볼 수 있다.
이제 Generator를 훈련할 때 볼 수 있었던 Discriminator의 Loss 값을 G.plot_progress( )를 통해 살펴보자.
여전히 Loss는 오직 Discriminator의 것이다. Generator에는 self.loss가 없다는 것을 명심하자.
Graph를 보면 초반에는 Discriminator가 Fake인지 Real인지 잘 분류를 못하는 현상을 확인할 수 있다. (0.25면 실제로는 0.5라는 애매한 결과가 나왔다는 것이고, 이는 0.0인지 1.0인지 확실히 구별을 못했다는 뜻이다)
중반에는 살짝 Loss가 올라간 것을 볼 수 있고, 이는 Generator가 어느정도 Discriminator를 속일 수 있을 정도로 성능이 향상했다는 것을 의미한다.
Train의 마지막에 도달할 때쯤 다시 Generator와 Discriminator 간의 균형이 생긴 것을 확인할 수 있다.
이제 Generator가 만들어낸 데이터를 보고 어떠한 패턴을 만들었는지 확인해보자.
G.forward(torch.FloatTensor([0.5]).to(device))
tensor([0.9341, 0.0501, 0.9323, 0.0510], device='mps:0',
grad_fn=<SigmoidBackward0>)
결과를 보면 Generator가 정말로 '1010 패턴'을 만들어냈다는 것을 확인할 수 있다.
즉, 1, 3번째의 값이 0.9 근처로 굉장히 크고, 2, 4번 째의 값이 0.05 근처의 값으로 작다는 것을 확인할 수 있다.
추가적인 실험으로 '1010 패턴'이 어떠한 식으로 훈련을 거치면 변화하는지 시각화해보자. 이를 위해서 빈 리스트 변수인 image_list를 훈련이 본격적으로 실행하기 전에 만들어놓고, 생성기의 결과를 1000회마다 저장한다. GAN 훈련 3단계 코드에 추가하면 된다.
# 1000 epoch 마다 image add
if epoch % 1000 == 0:
image_list.append(G.forward(torch.FloatTensor([0.5]).to(device)).detach().cpu().numpy())
Generator의 output tensor에서 numpy 행렬로 값을 추출하기 위해 numpy( )함수 호출 전 detach( ) 함수를 통해 계산 그래프를 통해 값을 떼어내는 과정이 필요하다.
문제는 torch.FloatTensor([0.5])가 디바이스로 옮겨지지 않았다는 것이다. 이 텐서도 G.forward 호출 시 MPS 디바이스로 옮겨야 한다.
또 다른 문제는 .cpu() 호출을 추가하여 GPU 또는 MPS 디바이스에서 계산된 텐서를 CPU로 옮기고, 그 다음에 .numpy() 호출을 통해 NumPy 배열 로 변환한다.
훈련을 마친 후, image_list는 10가지 패턴을 가지고 있다. (1000 epoch를 간격으로 추출했으므로)
10개의 패턴과 각각 4개의 값을 가진 리스트를 $10 \times 4$의 numpy 행렬로 변환하고 이를 대각선을 기준으로 뒤집어주어 변화 방향이 오른쪽으로 향하게끔 설정한다.
plt.figure(figsize=(16, 8))
plt.imshow(np.array(image_list).T, interpolation='none', cmap='Blues')
해당 Chart는 Generator가 어떻게 변화해가는지 명확하게 보여준다.
훈련이 반 정도 지날 때는 Generator가 '1010 패턴'을 명확히 보여주기 시작하고 후반에 다다를수록 더욱 뚜렷한 패턴이 형성된다.
2.7) 정리
GAN을 훈련하는 좋은 방법은 다음과 같다.
1) Real Dataset을 미리 살펴본다.
2) Discriminator가 적어도 Real Data와 Arbitrary Noise를 구별할 수 있는 성능은 지니는지 확인한다.
3) Non-Trained Generator가 올바른 형태의 Data를 만들어내는지 확인한다.
4) Loss가 어떻게 변하는지 시각화한다.
잘 훈련된 GAN은 생성된 가짜 이미지와 실제 이미지를 잘 구별하지 못하는 상태이다. 즉 출력은 0.5로서 0.0과 1.0의 중간에 해당하는 값이며, MSELoss의 이상적인 값은 0.25이다.
Generator와 Discriminator를 각각 따로 시각화하는 것은 유용하다.
Generator의 Loss는 Generated Data로부터 발생한 Discriminator의 Loss이다.