본문 바로가기

Book/GAN In Action

8장. CGAN

반응형

  이번 장에서는 생성자와 판별자를 훈련하는 데 모두 레이블을 사용하는 CGAN(Conditional generative adversarial network)에 대해 배운다.

 

동기

  앞서 보았듯이 GAN을 사용하면 간단한 손글씨 숫자에서 실제 같은 사람의 얼굴까지 생성할 수 있다. 훈련 데이터셋을 바꿔서 GAN이 학습하는 샘플의 종류를 조정할 수 있지만, GAN이 생성하는 샘플의 특징은 전혀 지정할 수 없다. 예를 들어 DCGAN은 실제와 같은 손글씨 숫자를 합성하지만 숫자 9가 아니라 숫자 7을 생성하도록 명령할 수 없다.

  10개의 클래스 중 하나에 속하는 샘플로 구성된 MNIST 같이 간단한 데이터셋에서는 큰 문제로 여겨지지 않을 수 있지만 만약 분류해야할 클래스의 종류가 많다면 원하는 가짜 샘플을 얻기 위해서는 수많은 시행이 필요할 것이다. ProGAN이 생성하는 이미지는 정확하지만 어떤 얼굴을 생성할 지는 제어할 수가 없다. 나이나 표정은 물론이고 남자 또는 여자 얼굴을 생성하도록 지시할 방법이 없다.

  만약 생성할 데이터의 종류를 결정할 수 있다면 수많은 애플리케이션을 만들 수 있다. 조건에 맞는 이미지를 생성한다고 생각해보자. 얼마나 많은 일들이 가능할 것인가..

  

CGAN이란

  CGAN을 훈련하는 동안 생성자는 훈련 데이터셋에 있는 각 레이블에 대해 실제 같은 샘플을 생성하는 법을 배운다. 판별자는 '진짜 샘플 - 레이블' 쌍과 '가짜 샘플 - 레이블' 쌍을 구별하는 법을 배운다. SGAN과는 달리 판별자가 진짜 샘플에 올바른 레이블을 할당하는 것을 학습한다. 마찬가지로 진짜 샘플과 가짜 샘플을 구분하는 것도 학습한다. CGAN의 판별자는 각 클래스를 구분하는 것을 학습하지 않는다. 판별자는 진짜 샘플 - 레이블 쌍만 받아들이고 샘플 - 레이블 쌍이 맞지 않는 것이나 가짜 샘플의 쌍은 거부한다.

  예를 들어, MNIST 데이터셋에서 (샘플, 레이블) 순으로 판별자가 데이터를 구분한다고 할 때 (3, 4)의 쌍이 들어왔을 때 앞의 3이 진짜이든 가짜이든 거부하도록 학습한다. 또한 판별자는 레이블과 이미지가 맞더라도 가짜 이미지 - 레이블 쌍은 거부하도록 학습한다.

  따라서 판별자를 속이기 위해서는 CGAN 생성자가 진짜처럼 보이는 샘플을 생성하는 것 뿐만 아니라 생성한 샘플이 레이블과 잘 맞도록 해야한다. 생성자가 완전히 훈련되면 레이블을 전달항여 CGAN으로 원하는 샘플을 생성할 수 있다.

 

CGAN 생성자

  생성자는 잡음 벡터 z와 레이블 y를 사용해 가짜 샘플 G(z,y) = x*|y를 합성한다.(y가 주어졌을 때 x* 또는 y 조건하에서 x*로 읽는다.) 가짜 샘플의 목표는 레이블이 주어졌을 때 진짜 샘플에 가능한 한 가깝게 보이는 것이다.

 

CGAN 판별자

  판별자는 진짜 샘플과 레이블 (x,y)를 받고 가짜 샘플과 이 샘플을 생성하는 데 사용한 레이블 (x*|y, y)를 받는다. 진짜 샘플 - 레이블 쌍으로 판별자는 진짜 데이터를 구별하고 그에 맞는 쌍을 판별하는 법을 배운다. 생성자가 만든 샘플에서는 가짜 이미지 - 레이블 쌍을 판별하는 법을 배운다. 따라서 진짜 샘플과 가짜 샘플을 구분하도록 학습한다.

  판별자는 입력이 진짜이고 올바른 쌍인지를 나타내는 하나의 확률을 출력한다.

 

<요약>

  생성자 판별자
입력 랜덤 벡터와 레이블 : (z,y) 판별자는 다음 입력을 받는다.
- 훈련 데이터셋의 샘플과 레이블 : (x,y)
- 주어진 레이블에 맞게 생성자가 만든 가짜 샘플과 해당 레이블 : (x*|y, y)
출력 주어진 레이블에 가능한 한 맞도록 생성된 가짜 샘플 : G(z,y) = x*|y 입력 샘플이 진짜이고 샘플 - 레이블 쌍이 맞는지 나타내는 하나의 확률
목표 레이블에 맞는 진짜처럼 보이는 가짜 샘플 생성하기 생성자가 만든 가짜 샘플 - 레이블 쌍과 훈련 데이터셋의 진짜 샘플 - 레이블 쌍을 구별하기

 

CGAN 구현하기

  이제 CGAN을 구현해보자.

 

설정

  첫 번째 단계는 모델을 만들 때 필요한 모듈과 라이브러리를 임포트 한다.

%matplotlib inline

import matplotlib.pyplot as plt
import numpy as np

from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import (Activation, BatchNormalization, Concatenate, Dense,
                                     Embedding, Flatten, Input, Multiply, Reshape)
from tensorflow.keras.layers import LeakyReLU
from tensorflow.keras.layers import Conv2D, Conv2DTranspose
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.optimizers import Adam

 

  이전과 마찬가지로 입력 이미지 크기와 잡음 벡터 z의 크기, 데이터셋에 있는 클래스 개수를 지정한다.

img_rows = 28
img_cols = 28
channels = 1

# 입력 이미지 차원
img_shape = (img_rows, img_cols, channels)

# 생성자 입력으로 사용될 잡음 벡터 크기
z_dim = 100

# 데이터셋에 있는 클래스 개수
num_classes = 10

 

CGAN 생성자

  이제 CGAN 생성자를 구현해보자. 이전 장들과 비슷하게 구현하지만 바뀐 점은 입력 처리 부분이다. 임베딩과 원소별 곱셈을 사용해 랜덤 잡음 벡터 z와 레이블 y를 하나의 표현으로 바꾼다. 이 코드가 하는 일은 다음과 같다.

  1. 레이블 y(0~9)를 받아 케라스 Embedding 층으로 z_dim 크기(랜덤 잡음 벡터의 길이)의 밀집 벡터로 변환한다.
  2. 케라스 Multiply 층으로 레이블 임베딩과 잡음 벡터 z를 합친다. 이 층의 이름에서 알 수 있듯이 동일한 길이 두 개의 벡터에 각 원소를 곱하여 곱셈 결과로 이루어진 하나의 벡터를 출력한다.

  이미지를 합성하는 CGAN 생성자 네트워크의 입력으로 이 결과 벡터를 주입한다. 먼저 레이블을 z와 동일한 크기의 벡터로 임베딩한다. 그 다음 임베딩된 레이블과 z의 원소끼리 곱한다. 이 조인트 표현을 CGAN 생성자 네트워크의 입력으로 사용한다.

def build_generator(z_dim):

    model = Sequential()

    # 완전 연결 층을 사용해 입력을 7×7×256 텐서로 변환합니다.
    model.add(Dense(256 * 7 * 7, input_dim=z_dim))
    model.add(Reshape((7, 7, 256)))

    # 7×7×256에서 14×14×128 텐서로 바꾸는 전치 합성곱 층
    model.add(Conv2DTranspose(128, kernel_size=3, strides=2, padding='same'))

    # 배치 정규화
    model.add(BatchNormalization())

    # LeakyReLU 활성화
    model.add(LeakyReLU(alpha=0.01))

    # 14×14×128에서 14×14×64 텐서로 바꾸는 전치 합성곱 층
    model.add(Conv2DTranspose(64, kernel_size=3, strides=1, padding='same'))

    # 배치 정규화
    model.add(BatchNormalization())

    # LeakyReLU 활성화
    model.add(LeakyReLU(alpha=0.01))

    # 14×14×64에서 28×28×1 텐서로 바꾸는 전치 합성곱 층
    model.add(Conv2DTranspose(1, kernel_size=3, strides=2, padding='same'))

    # tanh 활성화 함수
    model.add(Activation('tanh'))

    return model
def build_cgan_generator(z_dim):

    # 랜덤 잡음 벡터 z
    z = Input(shape=(z_dim, ))

    # 조건 레이블: 정수 0-9까지 생성자가 만들 숫자
    label = Input(shape=(1, ), dtype='int32')

    # 레이블 임베딩: 
    # ----------------
    # 레이블을 z_dim 크기 밀집 벡터로 변환하고 
    # (batch_size, 1, z_dim) 크기 3D 텐서를 만듭니다.
    label_embedding = Embedding(num_classes, z_dim, input_length=1)(label)

    # 임베딩된 3D 텐서를 펼쳐서 (batch_size, z_dim) 크기 2D 텐서로 바꿉니다.
    label_embedding = Flatten()(label_embedding)

    # 벡터 z와 레이블 임베딩의 원소별 곱셈
    joined_representation = Multiply()([z, label_embedding])

    generator = build_generator(z_dim)

    # 주어진 레이블에 대한 이미지 생성
    conditioned_img = generator(joined_representation)

    return Model([z, label], conditioned_img)

 

CGAN 판별자

  이전 절과 비슷하게 입력 이미지와 레이블을 다루는 부분을 제외하면 비슷한 구조이다. 여기서도 케라스 Embedding 층을 사용해 입력 레이블을 밀집 벡터로 변환한다. 하지만 입력이 평평한 벡터인 생성자와 달리 판별자는 3차원 이미지를 입력으로 받는다. 이를 위해서는 다음과 같은 처리가 필요하다.

  1. (0~9) 정수 레이블을 받아 케라스 Embedding 층으로 이미지를 펼친 길이인 28 x 28 x 1 = 784 크기 밀집 벡터로 변환한다.
  2. 레이블 임베딩을 이미지 차원(28 x 28  x 1)으로 바꾼다.
  3. 크기를 바꾼 레이블 임베딩을 이에 상응하는 이미지와 연결하여 28 x 28 x 2 크기의 표현으로 합친다. 이미지 위에 임베딩된 레이블로 도장을 찍는다고 생각할 수 있다.
  4. 이미지 - 레이블 조인트 표현을 CGAN 판별자 네트워크의 입력으로 주입한다. 새 입력 크기에 맞게 모델의 입력 차원을 28 x 28 x 2로 조정해야 한다.
def build_discriminator(img_shape):

    model = Sequential()

    # 28×28×2에서 14×14×64 텐서로 바꾸는 합성곱 층
    model.add(
        Conv2D(64,
               kernel_size=3,
               strides=2,
               input_shape=(img_shape[0], img_shape[1], img_shape[2] + 1),
               padding='same'))

    # LeakyReLU 활성화 함수
    model.add(LeakyReLU(alpha=0.01))

    # 14×14×64에서 7×7×64 텐서로 바꾸는 합성곱 층
    model.add(
        Conv2D(64,
               kernel_size=3,
               strides=2,
               padding='same'))

    # LeakyReLU 활성화 함수
    model.add(LeakyReLU(alpha=0.01))

    # 7×7×64에서 3×3×128 텐서로 바꾸는 합성곱 층
    model.add(
        Conv2D(128,
               kernel_size=3,
               strides=2,
               padding='same'))

    # LeakyReLU 활성화 함수
    model.add(LeakyReLU(alpha=0.01))

    # 시그모이드 활성화 함수를 사용한 출력층
    model.add(Flatten())
    model.add(Dense(1, activation='sigmoid'))

    return model
def build_cgan_discriminator(img_shape):

    # 입력 이미지
    img = Input(shape=img_shape)

    # 입력 이미지의 레이블
    label = Input(shape=(1, ), dtype='int32')

    # 레이블 임베딩: 
    # ----------------
    # 레이블을 z_dim 크기의 밀집 벡터로 변환하고 
    # (batch_size, 1, 28×28×1) 크기의 3D 텐서를 만듭니다.
    label_embedding = Embedding(num_classes,
                                np.prod(img_shape),
                                input_length=1)(label)

    # 임베딩된 3D 텐서를 펼쳐서 (batch_size, 28×28×1) 크기의 2D 텐서를 만듭니다.
    label_embedding = Flatten()(label_embedding)

    # 레이블 임베딩 크기를 입력 이미지 차원과 동일하게 만듭니다.
    label_embedding = Reshape(img_shape)(label_embedding)

    # 이미지와 레이블 임베딩을 연결합니다.
    concatenated = Concatenate(axis=-1)([img, label_embedding])

    discriminator = build_discriminator(img_shape)

    # 이미지-레이블 쌍을 분류합니다.
    classification = discriminator(concatenated)

    return Model([img, label], classification)

 

모델 생성

  이제 CGAN 판별자와 생성자 모델을 만들고 컴파일 한다. 생성자를 훈련하는 데 사용하는 연결 모델에서 동일한 입력 레이블이 생성자와 판별자에게 전달된다.(생성자는 샘플을 생성하기 위해, 판별자에게는 예측을 만들기 위해서)

def build_cgan(generator, discriminator):

    # 랜덤 잡음 벡터 z
    z = Input(shape=(z_dim, ))

    # 이미지 레이블
    label = Input(shape=(1, ))

    # 레이블에 맞는 이미지 생성하기
    img = generator([z, label])

    classification = discriminator([img, label])

    # 생성자 -> 판별자 연결 모델 
    # G([z, label]) = x* 
    # D(x*) = 분류
    model = Model([z, label], classification)

    return model
# 판별자 만들고 컴파일하기
discriminator = build_cgan_discriminator(img_shape)
discriminator.compile(loss='binary_crossentropy',
                      optimizer=Adam(learning_rate=0.00001),
                      metrics=['accuracy'])

# 생성자 만들기
generator = build_cgan_generator(z_dim)

# 생성자를 훈련하는 동안 판별자 모델 파라미터를 고정하기
discriminator.trainable = False

# 생성자를 훈련하기 위해 고정된 판별자로 CGAN 모델 만들고 컴파일하기
cgan = build_cgan(generator, discriminator)
cgan.compile(loss='binary_crossentropy', optimizer=Adam())

 

훈련

 

<CGAN 훈련 알고리즘>

  각 훈련 반복에서 다음을 수행한다.

  1. 판별자를 훈련한다.
    1. 진짜 샘플과 레이블의 랜덤한 미니배치 (x,y)를 받는다.
    2. 미니배치에 대한 D((x,y))를 계산하고 이진 분류 손실을 역전파하여 세타(D)를 업데이트하고 손실을 최소화한다.
    3. 랜덤 잡음 벡터와 클래스 레이블의 미니배치 (z,y)를 받고 가짜 샘플의 미니배치 G(z,y) = x*|y를 생성한다.
    4. 미니배치에 대해 D(x*|y, y)를 계산하고 이진 분류 손실을 역전파하여 세타(D)를 업데이트하고 손실을 최소화한다.
  2. 생성자를 훈련한다.
    1. 랜덤 잡음 벡터와 클래스 레이블의 미니배치 (z,y)를 받고 가짜 샘플의 미니배치 G(z,y) = x*|y를 생성한다.
    2. 주어진 미니배치에 대해 D(x*|y, y)를 계산하고 이진 분류 손실을 역전파하여 세타(D)를 업데이트하고 손실을 최대화한다.
accuracies = []
losses = []


def train(iterations, batch_size, sample_interval):

    # MNIST 데이터셋을 로드합니다.
    (X_train, y_train), (_, _) = mnist.load_data()

    # [0, 255] 사이 흑백 픽셀 값을 [–1, 1]로 스케일 변환합니다.
    X_train = X_train / 127.5 - 1.
    X_train = np.expand_dims(X_train, axis=3)

    # 진짜 이미지의 레이블: 모두 1
    real = np.ones((batch_size, 1))

    # 가짜 이미지의 레이블: 모두 0
    fake = np.zeros((batch_size, 1))

    for iteration in range(iterations):

        # -------------------------
        #  판별자를 훈련합니다.
        # -------------------------

        # 진짜 이미지와 레이블로 이루어진 랜덤한 배치를 얻습니다.
        idx = np.random.randint(0, X_train.shape[0], batch_size)
        imgs, labels = X_train[idx], y_train[idx]

        # 가짜 이미지 배치를 생성합니다.
        z = np.random.normal(0, 1, (batch_size, z_dim))
        gen_imgs = generator.predict([z, labels])

        # 판별자를 훈련합니다.
        d_loss_real = discriminator.train_on_batch([imgs, labels], real)
        d_loss_fake = discriminator.train_on_batch([gen_imgs, labels], fake)
        d_loss = 0.5 * np.add(d_loss_real, d_loss_fake)

        # ---------------------
        #  생성자를 훈련합니다.
        # ---------------------

        # 잡음 벡터의 배치를 생성합니다.
        z = np.random.normal(0, 1, (batch_size, z_dim))

        # 랜덤한 레이블의 배치를 얻습니다.
        labels = np.random.randint(0, num_classes, batch_size).reshape(-1, 1)

        # 생성자를 훈련합니다.
        g_loss = cgan.train_on_batch([z, labels], real)

        if (iteration + 1) % sample_interval == 0:

            # 훈련 과정을 출력합니다.
            print("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" %
                  (iteration + 1, d_loss[0], 100 * d_loss[1], g_loss))

            # 훈련이 끝난 후 그래프를 그리기 위해 손실과 정확도를 저장합니다.
            losses.append((d_loss[0], g_loss))
            accuracies.append(100 * d_loss[1])

            # 생성한 이미지 샘플을 출력합니다.
            sample_images()

 

샘플 이미지 출력

  이 함수를 사용하여 훈련하는 동안 생성자가 만든 이미지 품질이 얼마나 향상되었는지 조사해보자. 

def sample_images(image_grid_rows=2, image_grid_columns=5):

    # 랜덤한 잡음을 샘플링합니다.
    z = np.random.normal(0, 1, (image_grid_rows * image_grid_columns, z_dim))

    # 0–9 사이의 이미지 레이블을 만듭니다.
    labels = np.arange(0, 10).reshape(-1, 1)

    # 랜덤한 잡음에서 이미지를 생성합니다.
    gen_imgs = generator.predict([z, labels])

    # 이미지 픽셀 값을 [0, 1] 사이로 스케일을 변환합니다.
    gen_imgs = 0.5 * gen_imgs + 0.5

    # 이미지 그리드를 설정합니다.
    fig, axs = plt.subplots(image_grid_rows,
                            image_grid_columns,
                            figsize=(10, 4),
                            sharey=True,
                            sharex=True)

    cnt = 0
    for i in range(image_grid_rows):
        for j in range(image_grid_columns):
            # 이미지 그리드를 출력합니다.
            axs[i, j].imshow(gen_imgs[cnt, :, :, 0], cmap='gray')
            axs[i, j].axis('off')
            axs[i, j].set_title("Digit: %d" % labels[cnt])
            cnt += 1

 

모델 훈련

  이제 구현한 모델을 실행해보자.

# 하이퍼파라미터를 설정합니다.
iterations = 20000
batch_size = 32
sample_interval = 1000

# 지정된 반복 횟수 동안 CGAN을 훈련합니다.
train(iterations, batch_size, sample_interval)

 

출력 검증

  생성자에게 0부터 9까지 행마다 다른 숫자를 만들도록 해본 결과이다. 각 숫자가 다른 필체로 쓰인 것을 보면 CGAN이 훈련 데이터셋의 각 레이블에 맞는 샘플을 생성하는 것 뿐만 아니라 훈련 데이터에 있는 다양성도 충분히 학습했다는 것을 알 수 있다.

# 그리드 차원을 설정합니다.
image_grid_rows = 10
image_grid_columns = 5

# 랜덤한 잡음을 샘플링합니다.
z = np.random.normal(0, 1, (image_grid_rows * image_grid_columns, z_dim))

# 생성할 이미지 레이블을 5개씩 준비합니다.
labels_to_generate = np.array([[i for j in range(5)] for i in range(10)])
labels_to_generate = labels_to_generate.flatten().reshape(-1, 1)

# 랜덤한 잡음에서 이미지를 생성합니다.
gen_imgs = generator.predict([z, labels_to_generate])

# 이미지 픽셀 값을 [0, 1] 사이로 스케일을 변환합니다.
gen_imgs = 0.5 * gen_imgs + 0.5

# 이미지 그리드를 설정합니다.
fig, axs = plt.subplots(image_grid_rows,
                        image_grid_columns,
                        figsize=(10, 20),
                        sharey=True,
                        sharex=True)

cnt = 0
for i in range(image_grid_rows):
    for j in range(image_grid_columns):
        # 이미지 그리드를 출력합니다.
        axs[i, j].imshow(gen_imgs[cnt, :, :, 0], cmap='gray')
        axs[i, j].axis('off')
        axs[i, j].set_title("Digit: %d" % labels_to_generate[cnt])  ## NEW
        cnt += 1

 

결론

  이 장에서는 우리가 원하는 가짜 샘플을 만들기 위해 어떻게 레이블을 사용하여 생성자와 판별자를 훈련하는지 알아보았다. CGAN을 활용한다면 이미지 대 이미지 변환 문제에 대한 범용 솔루션으로 사용할 수 있다. 이는 한 종류의 이미지를 다른 종류의 이미지로 변환하는 문제이다. 이미지 대 이미지 변환 애플리케이션은 흑백 사진을 컬러 사진으로 바꾸는 것부터 주간 사진을 야간 사진으로 바꾸고 지도 뷰에서 위성 뷰를 합성하는 것까지 다양하다.

  CGAN 패러다임을 기반으로 한 가장 성공한 초기 구현 중 하나는 pix2pix이다. 이는 이미지 쌍을 사용하여 한 도메인에서 다른 도메인으로 이미지를 변환하는 방법을 학습한다. 하지만 pix2pix는 처음 발표된 후 1년 만에 이미지 대 이미지 작업에서 다른 GAN 모델이 이 성능을 압도 했을 뿐만 아니라 이미지 쌍도 필요없게 되었다.. CycleGan이라는 것인데 이는 각 도메인을 대표하는 두 그룹의 이미지만 필요하다. 이를 다음 장에서 학습하도록 하자.

반응형

'Book > GAN In Action' 카테고리의 다른 글

7장. SGAN  (0) 2024.08.08
6장. ProGAN  (0) 2024.08.07
5장. GAN 평가의 어려움  (0) 2024.08.06
4장. DCGAN  (0) 2024.08.01
3장. GAN 구현하기  (0) 2024.07.30