1. Chapter 10 - Condtional GAN
Chapter 10에서는 앞에서 설계한 DCGAN등에서 발생하는 다양성 없이 이미지를 생성하는 Mode Collapse를 해결하기 위해
GAN이 생성하는 이미지를 단일한 클래스로 고정한 채 다양한 이미지를 생성할 수 있도록 Conditional Input을 넣어주는 CGAN (Conditional GAN)을 설계한다.
예를 들어 GAN에 숫자 3을 표현하는 다양한 이미지를 생성하라고 요청할 수 있다.
또는 훈련 데이터에 감정을 나타내는 클래스가 있고 얼굴 이미지로 훈련을 하고 있다면
행복한 표정의 얼굴만 만들어달라고 요청할 수도 있다.
1.1) Conditional GAN 구조
Generator
훈련된 GAN Generator가 주어진 Class에 해당하는 이미지를 생성하게 하려면, 일단 무슨 Class를 목표로 하는지 Generator에 알려주어야 한다.
즉, Generator에 임의의 Seed (Samling된 Noise)와 함께 어떠한 이미지를 원하는지 (Class 정보) Conditional Input을 넣어줘야 한다.
Discriminator
이전에는 Discriminator가 Generator에서 생성한 Fake Image와 실제 Training Set에서 나온 Real Image를 구별하는 것이었다면,
Conditional Input이 추가된 지금은 Class Label과 Input Image 사이의 관계를 학습해야 된다.
특정 Class Label에 대해 어떠한 이미지가 해당 Class에 대응되는지 알아야 하기 때문이다.
학습을 하지 못한다면 Discriminator가 Generator에 줄 수 있는 피드백도 없으며
Class Label과 Image를 연관 짓지도 못한다.
즉, Discriminator에도 Conditional Input (Class Label)을 Input Image와 같이 제공해줘야 한다.
아래의 사진은 Conditional GAN의 구조를 나타낸 것이다.
주요 차이점은 Discriminator과 Generator 모두 Image Data외에도 Conditional Input (Class Label)을 추가로 입력받는다.
CGAN 구조의 핵심 Point (3-step training loop)를 정리하자면:
Generator Input: Gaussian Noise $z$와 임의의 Class Label $y_{1}$를 Concatenate
Discriminator Input(Fake): Gaussiasn Noise $z$와 임의의 Class Label $y_{1}$을 Concatenate 후,
Generator Ouput $G(z)$와 동일한 임의의 Class Label $y_{1}$를 Concatenate하여 $D$의 input에 넣어준다.
Discriminator Input(Real): Real Training Input Image $x$와 실제 Class Label $y_{2}$를 Concatenate
1.2) CGAN - Discriminator
이전 Chapter에서 만들었던 FC Layer MNIST GAN을 수정해보자.
Image pixel data와 Class Label 정보를 동시에 받도록 Discriminator의 forward를 업데이트 하자.
def forward(self, image_tensor, label_tensor):
# seed & label concatenate
inputs = torch.cat([image_tensor, label_tensor])
return self.model(inputs)
forward( ) 함수에서 Image tensor와 Label tensor를 동시에 받게 하고 이를 단순히 concatenate 시키면 된다.
Label Tensor는 one-hot encoding 되어 처리된 tensor로서 Dataset Class에서 이미 준비해둔 tensor다.
MNISTDataset Class에서의 image_values는 image_tensor, target는 label_tensor로 각각 들어간다.
def __getitem__(self, index):
# image label
label = self.data_df.iloc[index, 0]
# one-hot encoding vector
target = torch.zeros(10)
target[label] = 1.0
# 0-255의 image를 0-1로 Normalize
image_values = torch.FloatTensor([self.data_df.iloc[index, 1:].values]) / 255.0
# label, image tensor, one-hot encoded tensor 반환
return label, image_values, target
torch.cat( ) 함수는 단순히 하나의 텐서를 다른 텐서에 잇는 역할을 한다.
Dataset class로부터 반환된 이미지 텐서의 길이는 784이고, label tensor의 길이는 10이므로
두 tensor를 이은 tensor의 길이는 794이다.
input의 크기를 확장했기에, Discriminator의 첫 번째 Layer 정의시 input size는 784 + 10이 되어야 한다.
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.model = nn.Sequential(
nn.Linear(784 + 10, 200),
nn.LeakyReLU(0.02),
nn.LayerNorm(200),
nn.Linear(200, 1),
nn.Sigmoid(),
)
마지막으로 Discriminator Class의 train( ) 함수 안에서 forward( )를 호출할 때 레이블을 추가한 것이다.
def train(self, inputs, label_tensor, targets):
inputs, label_tensor, targets = inputs.to(device), label_tensor.to(device), targets.to(device)
# nn.BCELoss()에서는 outputs, targets 모두 torch.Size([1])를 요구하므로 차원 조정
# outputs는 원래 torch.Size([N, 1]): N은 Batch size
outputs = self.forward(inputs, label_tensor).view(-1)
loss = self.loss_function(outputs, targets)
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
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
이후 one-hot encoding vector를 size에 맞게 임의로 만들어내는 함수를 generate_random_one_hot( )이라 하자.
Generator의 input $z$ noise 그리고 Generator의 output $G(z)$와 concatenate 될 임의의 class label이다.
코드를 보면 임의의 이미지 (generate_random_image(784))와 함께
임의의 class label (generate_random_one_hot)이 필요하다는 것을 알 수 있다.
def generate_random_image(size):
return torch.rand(size)
def generate_random_seed(size):
return torch.randn(size)
# size는 정수로 지정해야 한다.
def generate_random_one_hot(size):
label_tensor = torch.zeros(size)
random_idx = torch.randint(0, size-1, (1,))
label_tensor[random_idx] = 1.0
return label_tensor
이제 Discriminator가 주어진 class label에 따라 input image를 제대로 분류할 수 있는지 성능을 테스트해보자.
Training Loop안에서 label tensor를 추가로 train( ) 함수에 전달하도록 수정해야 된다.
# %%time
D = Discriminator().to(device)
for label, image_data_tensor, target_tensor in mnist_dataset:
# real data
D.train(image_data_tensor.squeeze(), target_tensor, torch.FloatTensor([1.0])) # image_data_tensor shape: [1 x 784] -> squeeze()
# arbitrary data
D.train(generate_random_image(784), generate_random_one_hot(10), torch.FloatTensor([0.0]))
image_data_tensor: shape = [1 x N]
target_tensor: shape = [N]
이므로 두 tensor의 shape을 모두 1-dimension으로 맞춘다.
Discriminator의 Loss Chart를 한 번 살펴보자.
기존의 MNIST GAN의 Discriminator Loss와 크게 다른 것은 없어 보이고, 잘 수렴하는 것을 확인할 수 있다.
1.3) CGAN - Generator
이제는 Generator를 한 번 다뤄보자.
Generator의 input에는 Seed(Sampled Noise)와 임의의 Connditional Input(label tensor)가 Concat된 형태로 들어간다.
따라서, Generator의 forward( ) 함수를 다음과 같이 수정해야 한다.
def forward(self, seed_tensor, label):
# seed와 label 결합
inputs = torch.cat([seed_tensor, label])
return self.model(inputs)
Generator의 Network의 첫 번째 Layer는 임의의 label tensor를 입력으로 받기에 10개의 추가적인 값을 다룰 수 있도록 수정한다.
class Generator(nn.Module):
def __init__(self):
# PyTorch Parent Class 초기화
super(Generator, self).__init__()
self.model = nn.Sequential(
nn.Linear(100 + 10, 200),
nn.LeakyReLU(0.02),
nn.LayerNorm(200),
nn.Linear(200, 784),
nn.Sigmoid(),
)
마지막으로 Generator Class의 train( ) 함수도 임의의 label tensor를 입력으로 받아야 하므로 아래와 같이 바꿔준다.
Generator에서 생성된 이미지 (g_output)들을 Discriminator의 forward( )함수에
동일한 임의의 label (label_tensor)과 함께 넘겨주도록 했다.
이는 Discriminator가 다른 label로 잘못 판단하는 것을 방지한다.
def train(self, D, inputs, label_tensor, targets):
inputs, label_tensor, targets = inputs.to(device), label_tensor.to(device), targets.to(device)
# 신경망 출력 계산
g_output = self.forward(inputs, label_tensor)
# 판별기로 전달
d_output = D.forward(g_output, label_tensor).view(-1)
# 오차 계산
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
1.4) CGAN - Training Loop
GAN의 Training Loop 또한 Real, Arbitrary한 Label Tensor를 각각 Discriminator와 Generator에 전달하도록 수정해야 한다.
# %%time
# Discriminator & Generator 생성
D = Discriminator().to(device)
G = Generator().to(device)
# Discriminator & Generator 훈련
for epoch in range(12):
for label, image_data_tensor, label_tensor in mnist_dataset:
# 1단계: Discriminator Real-Image에 대해 훈련
D.train(image_data_tensor.squeeze(), label_tensor, torch.FloatTensor([1.0]))
# 2단계: Discriminator Fake-Image에 대해 훈련
# 임의의 one-hot encoding된 값을 label로 이용
random_label = generate_random_one_hot(10)
# Generator Weight Update X: detach() 함수를 사용
D.train(
G.forward(generate_random_seed(100).to(device), random_label.to(device)).detach(),
random_label,
torch.FloatTensor([0.0]),
)
# 3단계: Generator가 만든 Fake-Image에 대해 Discriminator가 Real_Image로 착각하게끔 훈련
# 임의의 one-hot encoding된 값을 label로 이용
random_label = generate_random_one_hot(10)
G.train(D, generate_random_seed(100), random_label, torch.FloatTensor([1.0]))
pass
D.train( G.forward(generate_random_seed(100).to(device), random_label.to(device)).detach(), random_label, torch.FloatTensor([0.0])) 에서 둘의 random_label이 동일한 이유는
noise $z$와 concat되는 임의의 class label $y_{1}$은 Generator의 output $G(z)$와도 concat되어 Discriminator의 input으로 들어가기 때문이다.
1, 2, 3 단계 훈련에서 사용되는 각 Conditional Input (Class Label)은 전부 다르기에, 2, 3단계에 들어가는 Arbitrary Class Label도 서로 다른 값이다.
생성된 이미지에 대해 Discriminator를 훈련할 때 (2단계), random_label 변수를 사용해서 매 batch 마다 Discriminator와 Generator 모두에 같은 임의의 label tensor를 넣는 것을 알 수 있다.
1.5) CGAN - Plot Images
1, 2, 3단계 Training 이후 정해진 Label에 대해 어떻게 이미지를 생성하는지 확인해보려면 Generator의 Class안에 plot_image( ) 함수를 넣으면 된다.
def plot_image(self, label):
label_tensor = torch.zeros(10)
label_tensor[label] = 1.0
# 2행 3열로 샘플 이미지 출력
f, axarr = plt.subplots(2, 3, figsize=(16, 8))
for i in range(2):
for j in range(3):
axarr[i, j].imshow(
self.forward(generate_random_seed(100).to(device), label_tensor.to(device)).detach().cpu().numpy().reshape(28, 28),
interpolation="none",
cmap="Blues",
)
pass
pass
pass
이 함수를 label을 정수로 받아, 이로부터 one-hot encoding된 tensor를 만들고 이를 Generator에 전달한다.
여섯 개의 서로 다른 noise로부터 sampling을 하고 Generator를 통해 여섯 개의 image로 mapping된다.
최종적으로 생성된 여섯 개의 이미지가 격자에 그려진다.
1.6) CGAN - Results
Discriminator의 Loss가 0에 가깝지 않고 오히려 상승하는 것을 알 수 있다.
이 현상은 상당히 긍정적인데, GAN의 이상적인 손실값이 0이 아니기 때문이다.
Generator의 Loss는 기존 GAN과 비슷해 보이지만, 자세히 보면 평균은 0이 아니고 이는 좋은 징조이다.
이는 GAN을 훈련할 때 추가적인 Class Label이 도움이 된다는 것이다.
Discriminator는 이미지가 실제인지 아닌지 판별할 때 도움이 되는 정보를 가지고 있고, 이것이 다시 Generator로 투입되었기에 이러한 결과가 나온 것이다.
plot_image(0)을 실행했더니 GAN이 숫자 0에 해당하는 이미지들을 생성했다.
즉, 실제로 CGAN은 숫자 0을 생성했고 Mode Collapse 또한 발생하지 않았기에 더더욱 의미가 있다고 볼 수 있다.