스터디/AI

[이론/Imple] 생성적 적대 신경망 (GAN) 2

_leezoee_ 2024. 5. 14. 15:10

이 게시물은 <만들면서 배우는 생성 AI 2판> 교재의 내용과 소스코드를 기반으로 실습한 내용을  기반으로 하고있다.

 

https://github.com/rickiepark/Generative_Deep_Learning_2nd_Edition/

 

GitHub - rickiepark/Generative_Deep_Learning_2nd_Edition: <만들면서 배우는 생성 AI 2판>의 코드 저장소

<만들면서 배우는 생성 AI 2판>의 코드 저장소. Contribute to rickiepark/Generative_Deep_Learning_2nd_Edition development by creating an account on GitHub.

github.com

 

 

와서스테인 GAN - 그레이디언트 페널티 (WGAN-GP)

 

2017년 와서스테인 GAN은 안정적인 GAN 훈련을 돕는 방식 중 하나로 제시되었다.

이진 크로스 엔트로피 대신 이 손실 함수를 사용하면 GAN이 더 안정적으로 수렴하게 된다.

 

와서스테인 손실은 1과 0 대신에 1, -1 을 사용한다. 또한 판별자 마지막 층에서 시그모이드 활성화 함수를 제거해 예측 p가 [0, 1] 범위에 국한되지 않고 [ -무한대, 무한대 ] 범위의 어떤 숫자도 될 수 있도록 한다.

 

WGAN의 판별자는 보통 비평자(critic)라고 부르며 확률 대신 점수를 반환한다.

WGAN 비평자는 진짜 이미지와 생성된 이미지에 대한 예측 사이의 차이를 최대화한다.

(=> WGAN 생성자는 비평자로부터 가능한 높은 점수를 받는 이미지를 생성하려고 함, 비평자를 속여 진짜 이미지라고 생각되도록)

 

이러한 과정에서 비평자의 범위가 [ -무한대, 무한대 ] 이기 때문에 와서스테인 손실은 아주 큰 값이 될 수 있음

일반적으로 신경망에서 큰 숫자는 피해야 하기 때문에 여기서 립시츠 제약이 등장하게 된다.

 

공부한 책에서는 기울기 노름(norm)이 1에서 벗어날 경우 모델에 불이익을 주는 '그레디언트 패널티 (gradient penalty)' 항을 비판자 손실 함수에 포함시켜 립시츠 제약 조건을 직접 강제하는 방식을 사용하였다.

 

모델 훈련 시 모든 곳에서 그레디언트를 계산할 수는 없으니 일부 지점에서만 그레디언트를 계산한다.

치우치지 않기 위해 진짜 이미지와 가짜 이미지 쌍을 연결한 직선 중 무작위로 포인트를 선택해 보간된 이미지를 사용한다.

 

가짜-진짜 사이 보간된 이미지 시각화

 

 

 

 

이제 WGAN 실습 코드를 진행한다.

 

사용할 데이터는 케글에 있는 CelebA 얼굴 데이터셋이다.

https://www.kaggle.com/datasets/jessicali9530/celeba-dataset

 

CelebFaces Attributes (CelebA) Dataset

Over 200k images of celebrities with 40 binary attribute annotations

www.kaggle.com

 

코드 작성 전 사용할 파라미터를 정의해둔다.

IMAGE_SIZE = 64
CHANNELS = 3
BATCH_SIZE = 512
NUM_FEATURES = 64
Z_DIM = 128
LEARNING_RATE = 0.0002
ADAM_BETA_1 = 0.5
ADAM_BETA_2 = 0.999
EPOCHS = 200
CRITIC_STEPS = 3
GP_WEIGHT = 10.0
LOAD_MODEL = False
ADAM_BETA_1 = 0.5
ADAM_BETA_2 = 0.9

 

데이터를 로드하고 전처리하는 소스를 작성한다.

# 데이터 로드
train_data = utils.image_dataset_from_directory(
    "./경로입력",
    labels=None,
    color_mode="rgb",
    image_size=(IMAGE_SIZE, IMAGE_SIZE),
    batch_size=BATCH_SIZE,
    shuffle=True,
    seed=42,
    interpolation="bilinear",
)


# 데이터 전처리, 이미지 정규화
def preprocess(img):
    img = (tf.cast(img, "float32") - 127.5) / 127.5
    return img


train = train_data.map(lambda x: preprocess(x))

 

판별자 모델을 정의하는 소스코드를 작성한다.

critic_input = layers.Input(shape=(IMAGE_SIZE, IMAGE_SIZE, CHANNELS))
x = layers.Conv2D(64, kernel_size=4, strides=2, padding="same")(critic_input)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2D(128, kernel_size=4, strides=2, padding="same")(x)
x = layers.LeakyReLU()(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(256, kernel_size=4, strides=2, padding="same")(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(512, kernel_size=4, strides=2, padding="same")(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(1, kernel_size=4, strides=1, padding="valid")(x)
critic_output = layers.Flatten()(x)

critic = models.Model(critic_input, critic_output)
critic.summary()

 

판별자 모델 summary 결과

 

 

 

생성자 모델을 정의하는 소스코드를 작성한다.

generator_input = layers.Input(shape=(Z_DIM,))
#입력을 1*1 형태로 변환
x = layers.Reshape((1, 1, Z_DIM))(generator_input)
x = layers.Conv2DTranspose(
    512, kernel_size=4, strides=1, padding="valid", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    256, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    128, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    64, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)

#출력 이미지의 채널 수에 맞게 필터 수를 조정, 탄젠트 활성화 함수를 사용해 출력
generator_output = layers.Conv2DTranspose(
    CHANNELS, kernel_size=4, strides=2, padding="same", activation="tanh"
)(x)
#생성자 모델 생성
generator = models.Model(generator_input, generator_output)
generator.summary()

생성자 모델 summary 결과

 

 

 

다음으로 WGAN-GP 모델을 구현한다

class WGANGP(models.Model):
    def __init__(self, critic, generator, latent_dim, critic_steps, gp_weight):
        super(WGANGP, self).__init__()
        self.critic = critic
        self.generator = generator
        self.latent_dim = latent_dim
        self.critic_steps = critic_steps
        self.gp_weight = gp_weight

    def compile(self, c_optimizer, g_optimizer):
        super(WGANGP, self).compile()
        self.c_optimizer = c_optimizer
        self.g_optimizer = g_optimizer
        self.c_wass_loss_metric = metrics.Mean(name="c_wass_loss")
        self.c_gp_metric = metrics.Mean(name="c_gp")
        self.c_loss_metric = metrics.Mean(name="c_loss")
        self.g_loss_metric = metrics.Mean(name="g_loss")

    @property
    def metrics(self):
        return [
            self.c_loss_metric,
            self.c_wass_loss_metric,
            self.c_gp_metric,
            self.g_loss_metric,
        ]

    #그레디언트 패널티 손실 함수
    def gradient_penalty(self, batch_size, real_images, fake_images):
        alpha = tf.random.normal([batch_size, 1, 1, 1], 0.0, 1.0) #배치에 있는 이미지마다 0~1사이 랜덤 숫자를 생성해 벡터 alpha에 저장
        diff = fake_images - real_images
        interpolated = real_images + alpha * diff #보간 이미지 계산

        with tf.GradientTape() as gp_tape:
            gp_tape.watch(interpolated)
            pred = self.critic(interpolated, training=True) #비평자에게 보간된 이미지 점수 요청

        grads = gp_tape.gradient(pred, [interpolated])[0] #보간된 이미지에 대해 예측 그레디언트 계산
        norm = tf.sqrt(tf.reduce_sum(tf.square(grads), axis=[1, 2, 3])) #해당 벡터의 L2 norm 계산
        gp = tf.reduce_mean((norm - 1.0) ** 2) # L2 norm과 1 사이 평균 제곱 거리 반환
        return gp

    def train_step(self, real_images):
        batch_size = tf.shape(real_images)[0]

        for i in range(self.critic_steps): #비평자 업데이트 
            random_latent_vectors = tf.random.normal(
                shape=(batch_size, self.latent_dim)
            )

            with tf.GradientTape() as tape:
                fake_images = self.generator(
                    random_latent_vectors, training=True
                )
                fake_predictions = self.critic(fake_images, training=True)
                real_predictions = self.critic(real_images, training=True)

                c_wass_loss = tf.reduce_mean(fake_predictions) - tf.reduce_mean(
                    real_predictions
                ) #비평자를 위한 와서스테인 손실 계산(진짜,가짜 이미지에 대한 평균 예측 차이)
                c_gp = self.gradient_penalty(
                    batch_size, real_images, fake_images
                ) # 그레디언트 패널티 항 계산
                c_loss = c_wass_loss + c_gp * self.gp_weight #비평자 손실 = 와서스테인 손실 + 그레디언트 패널티

            c_gradient = tape.gradient(c_loss, self.critic.trainable_variables)
            self.c_optimizer.apply_gradients(
                zip(c_gradient, self.critic.trainable_variables)
            ) #비평자의 가중치 업데이트

        random_latent_vectors = tf.random.normal(
            shape=(batch_size, self.latent_dim)
        )
        with tf.GradientTape() as tape:
            fake_images = self.generator(random_latent_vectors, training=True)
            fake_predictions = self.critic(fake_images, training=True)
            g_loss = -tf.reduce_mean(fake_predictions) #생성자를 위한 와서스테인 손실 계산

        gen_gradient = tape.gradient(g_loss, self.generator.trainable_variables)
        self.g_optimizer.apply_gradients(
            zip(gen_gradient, self.generator.trainable_variables)
        ) #생성자 가중치 업데이트

        self.c_loss_metric.update_state(c_loss)
        self.c_wass_loss_metric.update_state(c_wass_loss)
        self.c_gp_metric.update_state(c_gp)
        self.g_loss_metric.update_state(g_loss)

        return {m.name: m.result() for m in self.metrics}

 

* WGAN-GP 구축 시 비평자에서 배치 정규화를 사용하면 안됨. 같은 배치 안에 이미지 사이 상관관계를 만들기 때문에 그레디언트 패널티 손실의 효과가 떨어질 수 있음. 

 

여기 까지 봤을 때 앞 게시글의 표준 GAN과 WGAN-GP 차이점을 정리한다

1. WGAN-GP는 와서스테인 손실을 사용

2. WGAN-GP는 진짜 이미지에 1, 가짜 이미지에 -1 을 레이블로 사용

3. 비평자 마지막 층에는 시그모이드 활성화 함수를 사용하지 않음

4. 비평자 손실 함수에 그레디언트 패널티 항을 추가

5. 생성자를 업데이트할 때마다 비평자를 여러번 훈련함

6. 비평자에는 배치 정규화 층이 없음

 

 

다음으로 모델을 컴파일 하고 에포크 출력 샘플을 확인해본다.

 

# GAN 컴파일
wgangp.compile(
    c_optimizer=optimizers.Adam(
        learning_rate=LEARNING_RATE, beta_1=ADAM_BETA_1, beta_2=ADAM_BETA_2
    ),
    g_optimizer=optimizers.Adam(
        learning_rate=LEARNING_RATE, beta_1=ADAM_BETA_1, beta_2=ADAM_BETA_2
    ),
)


class ImageGenerator(callbacks.Callback):
    def __init__(self, num_img, latent_dim):
        self.num_img = num_img
        self.latent_dim = latent_dim

    def on_epoch_end(self, epoch, logs=None):
        if epoch % 10 != 0: 
            return
        random_latent_vectors = tf.random.normal(
            shape=(self.num_img, self.latent_dim)
        )
        generated_images = self.model.generator(random_latent_vectors)
        generated_images = generated_images * 127.5 + 127.5
        generated_images = generated_images.numpy()
        display(
            generated_images,
            save_to="./경로/이미지_%03d.png" % (epoch),
            cmap=None,
        )
        
wgangp.fit(
    train,
    epochs=EPOCHS,
    steps_per_epoch=2,
    callbacks=[
        model_checkpoint_callback,
        tensorboard_callback,
        ImageGenerator(num_img=10, latent_dim=Z_DIM),
    ],
)

 

11 에포크 출력 이미지

 

61 에포크 출력 이미지

 

131 에포크 출력 이미지

 

191 에포크 출력 이미지

 

 

 

VAE 와 비교하면 GAN 이미지가 전반적으로 더 선명하게 나오는 편인데, 일반적으로 VAE는 색깔의 경계를 후리게 하여 부드러운 이미지를 만드는 경향이 있는 반면 GAN은 선명하고 형태가 뚜렷한 이미지를 만든다.

 

 

최종 이미지 생성

z_sample = np.random.normal(size=(10, Z_DIM))
imgs = wgangp.generator.predict(z_sample)
display(imgs, cmap=None)

 

 

 

GAN이 VAE 보다 일반적으로 더 훈련이 어렵고, 시간도 더 오래 걸린다.

하지만 요즘 GAN 기반 최신 생성 모델도 많아지고, 대규모 GAN 훈련에 GPU도 활용되면서 점차 나아지고 있다고 볼 수 있다.

 

 

 

 

 

조건부 GAN (CGAN)

 

조건부 GAN (conditional GAN, CGAN)은 GAN 구조를 확장한 것으로 조건을 부여한 GAN을 말한다,

CGAN은 레이블과 관련된 추가 정보를 생성자, 비평자에 전달한다

생성자는 이 정보를 원핫 인코딩된 벡터로 잠재 공간 샘플에 단순히 추가하고, 비평자에서는 레이블 정보를 RGB 이미지에 추가 채널로 추가한다.

(CGAN에서 비평자는 추가정보를 참고 할 수 있으므로 생성자는 레이블과 출력이 일치하는지 계속 확인해야한다.)

 

CGAN에 있는 생성자와 비평자 입력,출력

 

 

 

 

소스코드 실습을 진행한다. (파라미터 정의, 데이터 전처리는 앞선 WGAN 소스와 동일)

 

먼저 GAN 비평자(critic) 입력 레이어를 정의한다.

이미지와 그 이미지의 레이블을 입력으로 받는 모델을 정의하는 소스로, CNN 아키텍쳐를 사용한다.

#이미지를 받는 입력 레이어
critic_input = layers.Input(shape=(IMAGE_SIZE, IMAGE_SIZE, CHANNELS))
#레이블을 받는 입력 레이어
label_input = layers.Input(shape=(IMAGE_SIZE, IMAGE_SIZE, CLASSES))
#이미지와 레입르을 결합해 하나의 입력으로 만듦 (axis=1을 사용해 마지막 차원을 기준으로 결합)
x = layers.Concatenate(axis=-1)([critic_input, label_input])

x = layers.Conv2D(64, kernel_size=4, strides=2, padding="same")(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2D(128, kernel_size=4, strides=2, padding="same")(x)
x = layers.LeakyReLU()(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(128, kernel_size=4, strides=2, padding="same")(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(128, kernel_size=4, strides=2, padding="same")(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(1, kernel_size=4, strides=1, padding="valid")(x)
critic_output = layers.Flatten()(x)

critic = models.Model([critic_input, label_input], critic_output)
critic.summary()

 

비평자 모델 summary 결과

 

 

 

다음은 생성자의 입력 레이어를 정의한다.

랜덤한 잠재 공간 벡터와 클래스 레이블을 입력으로 받아 이미지를 생성하는 생성자 모델을 정의한다.

먼저 입력들이 결합되고, 이후 전치 합성곱을 통해 이미지를 생성한다.

마지막 출력 레이어에서는 tanh(하이퍼볼릭 탄젠트) 활성화 함수를 사용해 이미지를 생성 출력값은 -1 ~ 1 값으로 정규화 된다.

#잠재 공간 랜덤 벡터를 받는 입력 레이어
generator_input = layers.Input(shape=(Z_DIM,))
#이미지의 클래스 레이블을 받는 입력 레이어
label_input = layers.Input(shape=(CLASSES,))
#잠재 공간 벡터, 클래스 레이블을 결합해 하나의 입력으로 만듦 (axis=-1 사용, 마지막 차원 기준으로 결합)
x = layers.Concatenate(axis=-1)([generator_input, label_input])
#결합된 입력을 재구성
x = layers.Reshape((1, 1, Z_DIM + CLASSES))(x)
x = layers.Conv2DTranspose(
    128, kernel_size=4, strides=1, padding="valid", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    128, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    128, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    64, kernel_size=4, strides=2, padding="same", use_bias=False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)

#활성화 함수로 tanh 사용, -1~1 사이 값으로 정규화
generator_output = layers.Conv2DTranspose(
    CHANNELS, kernel_size=4, strides=2, padding="same", activation="tanh"
)(x)
generator = models.Model([generator_input, label_input], generator_output)
generator.summary()

생성자 모델 summary 결과

 

 

다음으로 CGAN을 만드는 소스코드를 작성한다.

 

#ConditionalWGAN 클래스 정의
class ConditionalWGAN(models.Model):
    def __init__(self, critic, generator, latent_dim, critic_steps, gp_weight):
        super(ConditionalWGAN, self).__init__()
        self.critic = critic
        self.generator = generator
        self.latent_dim = latent_dim
        self.critic_steps = critic_steps
        self.gp_weight = gp_weight

    def compile(self, c_optimizer, g_optimizer):
        super(ConditionalWGAN, self).compile(run_eagerly=True)
        self.c_optimizer = c_optimizer
        self.g_optimizer = g_optimizer
        self.c_wass_loss_metric = metrics.Mean(name="c_wass_loss")
        self.c_gp_metric = metrics.Mean(name="c_gp")
        self.c_loss_metric = metrics.Mean(name="c_loss")
        self.g_loss_metric = metrics.Mean(name="g_loss")

    @property
    def metrics(self):
        return [
            self.c_loss_metric,
            self.c_wass_loss_metric,
            self.c_gp_metric,
            self.g_loss_metric,
        ]

    #기울기 패널티 계산 메소드 작성
    def gradient_penalty(
        self, batch_size, real_images, fake_images, image_one_hot_labels
    ):
        alpha = tf.random.normal([batch_size, 1, 1, 1], 0.0, 1.0)
        diff = fake_images - real_images
        interpolated = real_images + alpha * diff

        with tf.GradientTape() as gp_tape:
            gp_tape.watch(interpolated)
            pred = self.critic(
                [interpolated, image_one_hot_labels], training=True
            )

        grads = gp_tape.gradient(pred, [interpolated])[0]
        norm = tf.sqrt(tf.reduce_sum(tf.square(grads), axis=[1, 2, 3]))
        gp = tf.reduce_mean((norm - 1.0) ** 2)
        return gp

    #훈련 스텝 메소드 작성
    def train_step(self, data):
        real_images, one_hot_labels = data #입력 데이터에서 이미지, 레이블 분리

        #원핫 인코딩된 벡터를 입력 이미지크기(64*64)와 같은 원핫 인코딩된 이미지로 확장
        image_one_hot_labels = one_hot_labels[:, None, None, :]
        image_one_hot_labels = tf.repeat(
            image_one_hot_labels, repeats=IMAGE_SIZE, axis=1
        )
        image_one_hot_labels = tf.repeat(
            image_one_hot_labels, repeats=IMAGE_SIZE, axis=2
        )

        batch_size = tf.shape(real_images)[0]

        for i in range(self.critic_steps):
            random_latent_vectors = tf.random.normal(
                shape=(batch_size, self.latent_dim)
            )

            with tf.GradientTape() as tape:
                #생성자에게 [랜덤한 잠재 벡터, 원핫 인코딩된 레이블 벡터]로 구성된 리스트 주입
                fake_images = self.generator(
                    [random_latent_vectors, one_hot_labels], training=True
                )

                #비평자에게 [가짜/진짜 이미지, 원핫 인코딩된 레이블 벡터]로 구성된 리스트 주입
                fake_predictions = self.critic(
                    [fake_images, image_one_hot_labels], training=True
                )
                real_predictions = self.critic(
                    [real_images, image_one_hot_labels], training=True
                )

                #와서스테인 손실 및 기울기 패널티 계산
                c_wass_loss = tf.reduce_mean(fake_predictions) - tf.reduce_mean(
                    real_predictions
                )
                c_gp = self.gradient_penalty(
                    batch_size, real_images, fake_images, image_one_hot_labels
                ) #기울기 손실 함수도 비평자 호출 시 전달할 원핫 인코딩 레이블 채널이 필요함
                c_loss = c_wass_loss + c_gp * self.gp_weight

            c_gradient = tape.gradient(c_loss, self.critic.trainable_variables)
            self.c_optimizer.apply_gradients(
                zip(c_gradient, self.critic.trainable_variables)
            )

        random_latent_vectors = tf.random.normal(
            shape=(batch_size, self.latent_dim)
        )

        with tf.GradientTape() as tape:
            fake_images = self.generator(
                [random_latent_vectors, one_hot_labels], training=True
            ) #비평자 훈련 스텝의 변경 사항은 생성자 훈련 스텝에도 적용
            fake_predictions = self.critic(
                [fake_images, image_one_hot_labels], training=True
            )
            g_loss = -tf.reduce_mean(fake_predictions)

        gen_gradient = tape.gradient(g_loss, self.generator.trainable_variables)
        self.g_optimizer.apply_gradients(
            zip(gen_gradient, self.generator.trainable_variables)
        )

        self.c_loss_metric.update_state(c_loss)
        self.c_wass_loss_metric.update_state(c_wass_loss)
        self.c_gp_metric.update_state(c_gp)
        self.g_loss_metric.update_state(g_loss)

        return {m.name: m.result() for m in self.metrics}
        
        
# GAN 생성
cgan = ConditionalWGAN(
    critic=critic,
    generator=generator,
    latent_dim=Z_DIM,
    critic_steps=CRITIC_STEPS,
    gp_weight=GP_WEIGHT,
)

 

 

다음으로 모델을 컴파일하고, 각 에포크마다 이미지를 확인할 수 있는 콜백 클래스를 정의한다.

해당 클래스에서는 주어진 latent 벡터로부터 두 가지 종류 이미지를 생성한다 (레이블 0, 레이블 1 이미지)

특정 에포크에서만 출력 되도록 조절할 수 있다 (if문 활용)

# GAN 컴파일
cgan.compile(
    c_optimizer=optimizers.Adam(
        learning_rate=LEARNING_RATE, beta_1=ADAM_BETA_1, beta_2=ADAM_BETA_2
    ),
    g_optimizer=optimizers.Adam(
        learning_rate=LEARNING_RATE, beta_1=ADAM_BETA_1, beta_2=ADAM_BETA_2
    ),
)

class ImageGenerator(callbacks.Callback):
    def __init__(self, num_img, latent_dim):
        self.num_img = num_img
        self.latent_dim = latent_dim

    #각 에포크 끝에서 호출되는 메소드
    def on_epoch_end(self, epoch, logs=None):
        #잠재 공간에서 랜덤 벡터 샘플링
        random_latent_vectors = tf.random.normal(
            shape=(self.num_img, self.latent_dim)
        )
        # 0 레이블에 대한 이미지 생성
        zero_label = np.repeat([[1, 0]], self.num_img, axis=0)
        generated_images = self.model.generator(
            [random_latent_vectors, zero_label]
        )
        #이미지를 [0, 255] 범위로 변환
        generated_images = generated_images * 127.5 + 127.5
        #이미지를 numpy 배열로 변환
        generated_images = generated_images.numpy()
        if epoch % 100 == 0: # 출력 횟수를 줄이기 위해
            display(
                generated_images,
                save_to="./경로/저장이름_%03d_label_0.png" % (epoch),
                cmap=None,
            )

        # 1 레이블에 대한 이미지 생성
        one_label = np.repeat([[0, 1]], self.num_img, axis=0)
        generated_images = self.model.generator(
            [random_latent_vectors, one_label]
        )
        #이미지를 [0, 255] 범위로 변환
        generated_images = generated_images * 127.5 + 127.5
        #이미지를 numpy 배열로 변환
        generated_images = generated_images.numpy()
        if epoch % 100 == 0: # 출력 횟수 조절
            display(
                generated_images,
                save_to="./경로/저장이름_%03d_label_1.png" % (epoch),
                cmap=None,
            )

 

 

모델 훈련을 시작한다. (예제는 에포크를 2000으로 했으나 추후 실습만 간단히 해보는거면 숫자를 작게 조절해야한)

 

history = cgan.fit(
    train,
    epochs=EPOCHS * 100,
    steps_per_epoch=1,
    callbacks=[
        model_checkpoint_callback,
        tensorboard_callback,
        ImageGenerator(num_img=10, latent_dim=Z_DIM),
    ],
)

101 번째 epochs 이미지

 

401번째 epochs 이미지

 

1101번째 epochs 이미지

 

 

 

 

다음은 생성한 모델을 저장하고 실제로 이미지를 생성해본다.

 

# 최종 모델 저장
generator.save("./models/generator")
critic.save("./models/critic")

# 0 레이블
z_sample = np.random.normal(size=(10, Z_DIM))
class_label = np.repeat([[1, 0]], 10, axis=0)
imgs = cgan.generator.predict([z_sample, class_label])
display(imgs, cmap=None) #display는 예제소스로 제공되는 것

레이블 0 이미지 생성 결과

 

 

# 1 레이블
z_sample = np.random.normal(size=(10, Z_DIM))
class_label = np.repeat([[0, 1]], 10, axis=0)
imgs = cgan.generator.predict([z_sample, class_label])
display(imgs, cmap=None)

레이블 1 이미지 생성 결과

 

 

 

 

 

 

 

여기까지 CGAN 에 대한 실습을 진행해보았다.

 

CGAN은 특정 원핫 인코딩 레이블을 생성자 입력에 전달해 출력을 제어할 수 있다.

 

예를 들어 금발이 아닌 경우는 벡터 [1,0]을 금발인 경우는 [0,1]을 전달하면,

샘플마다 랜덤한 잠재 벡터는 동일하게 유지하고 조건부 레이블 벡터만 변경한다.

CGAN이 레이블 벡터를 사용해 이미지의 머리카락 금발 속성만 제어하는 방법을 학습하게 되는 것이다.

이로써 얼굴의 나머지 부분은 거의 변하지 않고 머리카락의 금발 여부만 변화하게 된다.

 

(=> GAN에서 개별 특성이 분리되도록 잠재 공간의 포인트를 구성하기 때문)

 

데이터셋에 레이블이 있는 경우 레이블로 생성된 출력에 조건을 부여할 필요가 없더라도 GAN의 입력에 레이블을 포함하는 것이 좋음 (일반적으로 생성된 이미지의 품질을 높이는 경향이 있음)