본문 바로가기
Book Review/[만들면서 배우는 생성형 AI] 리뷰

4장. 생성적 적대 신경망(GAN)

by 3n952 2024. 4. 17.

 

4장의 Generative adversarial network에 대하여 학습해 보자.
GAN의 어떠한 구조적 특징을 가지고 있는지, 이에 대한 문제점은 무엇이 있는지 파악한 다음,
이를 와서스테인 GAN과 WGAN이 어떻게 해결하고자 하는지 알아보자!

 

1. GAN의 두 네트워크 구조

GAN은 General Adversarial Network의 약자로 생성적 적대 신경망이라고 부른다.

이를 해석해 보면 "적대적인 두 신경망으로 결과를 생성한다."라고 볼 수 있다.

어떤 아이디어가 이를 가능하게 할까?

이를 이해하기 위해서는 우선 GAN의 구조에 대해 알 필요가 있다.

 

GAN은 생성자(generator)와 판별자(discriminator)가 서로를 이기려고 하는 과정을 통해 의미 있는 샘플을 생성하는 것이다.(서로 적대적 관계라고 생각하면 된다.)

간략하게 표현하면 그림 4-2와 같은 구조를 가지고 있다.

생성자는 랜덤함 잡음으로부터 원래 데이터셋에서 샘플링한 것과 같은 샘플을 추출하는 것에 관심이 있고,

판별자는 원래 데이터 셋에서 샘플링한 것인지 생성자가 만든 위조품인지 예측하는 것에 관심이 있다.

 

따라서 생성자는 최대한 '진짜'(원래 데이터셋에서 샘플링한 것) 같은 샘플을 만들어야 하고,

판별자는 생성자가 만든 이미지가 진짜인지 아닌지 잘 파악해야 한다.

이러한 과정에서 생성자는 더 '진짜'같은 샘플을 만들어 낼 것이고 판별자는 더 '진짜'같아진 이미지를 더 잘 구분한다. 이 과정이 반복되면서 결국 생성자가 더 더 '진짜'같은 샘플을 만들어내게 하는 것이 GAN의 목표이다.

 

 

2. DCGAN(심층 합성곱 GAN)

위의 구조를 차용하여 심층 합성곱 GAN이 등장했다.

이를 통해 보다 사실적인 이미지를 생성하는 것에 성공했다.(그 당시에..)

 

앞서 언급한 것과 같이 DCGAN은 생성자 network와 판별자 network가 필요하다.

생성자는 Autoencoder의 decoder부분과 같은 역할을 한다.

반면 판별자는 image classification의 역할을 수행한다.

따라서 생성자는 Transposed conv 연산을 수행하여 원본과 같은 크기의 결과를 출력할 것이고,

판별자는 conv 연산을 통해 feature map을 1*1 크기로 조정하고 이를 Flatten 하여 0~1 사이의 결과를 출력하게 된다.

 

코드 예시는 다음과 같다.

#discriminator - 판별자 역할 block : image classification과 동일

discriminator_input = layers.Input(shape=(IMAGE_SIZE, IMAGE_SIZE, CHANNELS))
x = layers.Conv2D(64, kernel_size=4, strides=2, padding="same", use_bias=False)(
    discriminator_input
)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
    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.Dropout(0.3)(x)
x = layers.Conv2D(
    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.Dropout(0.3)(x)
x = layers.Conv2D(
    512, 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.Dropout(0.3)(x)
x = layers.Conv2D(
    1,
    kernel_size=4,
    strides=1,
    padding="valid",
    use_bias=False,
    activation="sigmoid",
)(x)
discriminator_output = layers.Flatten()(x)

discriminator = models.Model(discriminator_input, discriminator_output)
discriminator.summary()

 

# Generator - 생성자 역할 block : VAE decoder부분과 동일

generator_input = layers.Input(shape=(Z_DIM,))
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",
    use_bias=False,
    activation="tanh",
)(x)
generator = models.Model(generator_input, generator_output)
generator.summary()

 

 

2-1. DCGAN의 훈련 과정

GAN은 두 가지 네트워크의 구조뿐만 아니라 서로 다른 네트워크가 어떻게 학습하는지 훈련 과정을 아는 것이 핵심이다.

판별자가 진짜라고 생각하도록 하는 생성자의 샘플링이 필요하다.

이를 위해서는 다음과 같은 훈련 과정이 이뤄진다.

 

 

우선 판별자는 진짜 샘플을 구분해야 하므로 훈련 세트의 샘플과 생성자의 출력을 합쳐서 훈련 세트를 통해 학습이 이뤄진다.

image classification과 같이 '진짜'의 샘플은 1, '가짜 ' 샘플은 0으로 라벨링이 되어 지도 학습으로 훈련이 된다.

반면 생성자는 판별자가 수행한 작업(생성된 이미지에 점수를 부여하고 높은 점수를 낸 이미지로 최적화하는 것)이 필요한 데, 학습 과정 중에 생성자 자신이 만든 '가짜'이미지를 판별자에 전달하여 그 샘플에 대한 점수를 얻는다. 진짜 샘플로 만드는 게 목표이므로 1로 라벨링이 된다. 

 

 

두 네트워크의 경우 손실 함수로 이진 크로스 엔트로피를 사용한다.

여기서 중요한 점은 한 번에 두 네트워크의 가중치가 업데이트되는 것이 아니라, 두 네트워크가 번갈아 업데이트되는 것이 중요하다.

예를 들어 생성자 네트워크를 훈련할 때 판별자 네트워크도 훈련이 된다면, 판별자가 생성자가 대충 만들어도 '진짜'라고 판단하도록 조정되기 때문이다.

우리의 목표는 '진짜' 같은 샘플링을 하는 생성자를 만드는 것이지 '가짜'같은 샘플을 '진짜'라고 판단하는 판별자가 아니다.

 

train step을 통해 훈련 루프를 구현해 보면 더욱 직관적으로 파악할 수 있다.

def train_step(self, real_images):
        # 잠재 공간에서 랜덤 포인트 샘플링
        batch_size = tf.shape(real_images)[0]
        random_latent_vectors = tf.random.normal(
            shape=(batch_size, self.latent_dim)
        )

        # 가짜 이미지로 판별자 훈련하기
        with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
            # 여기서부터 
            generated_images = self.generator(
                random_latent_vectors, training=True
            )
            real_predictions = self.discriminator(real_images, training=True)
            fake_predictions = self.discriminator(
                generated_images, training=True
            )

            real_labels = tf.ones_like(real_predictions)
            real_noisy_labels = real_labels + NOISE_PARAM * tf.random.uniform(
                tf.shape(real_predictions)
            )
            fake_labels = tf.zeros_like(fake_predictions)
            fake_noisy_labels = fake_labels - NOISE_PARAM * tf.random.uniform(
                tf.shape(fake_predictions)
            )

            d_real_loss = self.loss_fn(real_noisy_labels, real_predictions)
            d_fake_loss = self.loss_fn(fake_noisy_labels, fake_predictions)
            d_loss = (d_real_loss + d_fake_loss) / 2.0

            g_loss = self.loss_fn(real_labels, fake_predictions)
            # 여기까지

        gradients_of_discriminator = disc_tape.gradient(
            d_loss, self.discriminator.trainable_variables
        )
        gradients_of_generator = gen_tape.gradient(
            g_loss, self.generator.trainable_variables
        )

        self.d_optimizer.apply_gradients(
            zip(gradients_of_discriminator, discriminator.trainable_variables)
        )
        self.g_optimizer.apply_gradients(
            zip(gradients_of_generator, generator.trainable_variables)
        )

        # 메트릭 업데이트
        self.d_loss_metric.update_state(d_loss)
        self.d_real_acc_metric.update_state(real_labels, real_predictions)
        self.d_fake_acc_metric.update_state(fake_labels, fake_predictions)
        self.d_acc_metric.update_state(
            [real_labels, fake_labels], [real_predictions, fake_predictions]
        )
        self.g_loss_metric.update_state(g_loss)
        self.g_acc_metric.update_state(real_labels, fake_predictions)

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

 

그럼에도 불구하고 DCGAN은 한계가 명확하다.

 

1. 훈련의 어려움. 

'판별자가 생성자보다 훨씬 뛰어난 경우 / 생성자가 판별자보다 훨씬 뛰어난 경우' 모두 각 손실 함수의 그레이디언트가 0에 수렴하여 전혀 훈련되지 않는 문제가 생긴다.

 

2. 손실 함수의 낮은 유용성

DCGAN은 생성자의 손실이 작을수록 생성된 이미지의 품질이 더 좋을 것이라고 생각된다.

하지만 생성자는 판별자에 의해서만 평가되고 판별자는 계속 향상되기 때문에 생성자의 손실과 이미지 품질 사이의 연관성이 부족하고,

이는 훈련 과정을 모니터링하는 데 어렵게 한다.

실제로 에폭이 증가함에 따라 이미지 품질이 좋아지지만 생성자 손실은 증가하는 모습을 보인다.

 

3. 다양한 하이퍼파라미터 

간단한 GAN도 튜닝해야 할 하이퍼파라미터가 상당히 많다.

구조의 크기, 배치 정규화, 드롭아웃, 잠재 공간 크기 등의 하이퍼파라미터를 고려해야 한다.

작은 하이퍼파라미터의 변화에도 민감하기 때문에 모델의 안정성을 높이고 민감한 하이퍼파라미터를 조정할 수 있어야 한다.

 

3.  Wasserstein GAN with gradient penalty - GAN의 한계(손실 함수이 낮은 유용성) 해결법 제시

안정적인 GAN의 훈련을 위해 다음 두 가지 속성을 고려한 GAN의 훈련법을 고안했다.

1. 생성자가 수렴하는 것과 샘플의 품질을 연관 짓는 의미 있는 손실 측정 방법

2. 최적화 과정의 안정성 향상

 

이진 크로스 엔트로피 대신 Wasserstein loss를 사용하면 더욱 안정적인 손실 함수의 수렴이 가능하다고 한다.

어떻게 이게 가능한지 알아보자.

 

3-1 Wasserstein(와서스테인) loss란?

와서스테인 손실함수는 다음과 같다.

 

와서스테인 손실 함수

 

이것이 원래의 이진 크로스 엔트로피와 비교하여 어떠한 점이 GAN 학습의 안정적인 수렴을 도와주는지 알아보자.

 

이진 크로스 엔트로피 손실함수는 식 4-1과 같다.

GAN의 판별자를 훈련하기 위해 진짜 이미지에 대한 예측 𝛲ᵢ = 𝐷(𝑥ᵢ)와 𝑦ᵢ = 1을 비교하고,

생성된 이미지에 대한 예측 𝛲ᵢ = 𝐷(G(zᵢ))와 𝑦ᵢ = 0을 비교하여 손실을 계산한다.

따라서 이를 최소화 하는 식은 식 4-2와 같이 나타낼 수 있다.

이에 따라 판별자 손실을 최소화하기 위해서는 𝐸x~px[logD(x)] + 𝐸z~pz[log(1 - D(G(z)))]가 최대가 되어야 한다.

각 항을 살펴보면, 𝐸x~px[logD(x)] 는 판별자가 실제 데이터 x를 실제로 판단한 확률의 로그 스케일에 대한 확률 가중 평균을 의미한다.

𝐸z~pz[log(1 - D(G(z)))]는 생성자가 만들어낸 가짜 데이터 G(z)를 가짜라고 판단한 확률의 로그스케일에 대한 확률 가중 평균을 의미한다. 이는 진짜는 진짜로, 가짜는 가짜로 판단하도록 판별자를 학습시키는 요인이 된다.

 

반면 생성자를 훈련하기 위해 생성된 이미지에 대한 예측 𝛲ᵢ = 𝐷(G(zᵢ))와 𝑦ᵢ = 1을 비교하여 손실을 계산한다.

이에 따라 생성자 손실을 최소화하기 위해서는 𝐸z~pz[log(D(G(z)))] 가 최대가 되어야 한다.

즉, 판별자 모델이 가짜 데이터 를 실제 데이터로 오인할 확률을 최대화하는데 중점을 둔다.

이는 판별자가 생성된 가짜 데이터를 얼마나 실제 데이터와 유사하다고 판단하는지를 측정하게 한다.

 

 

다시 와서스테인 손실로 돌아오자.

우선 와서스테인 손실은 타깃값을 1과 0 대신 타겟값으로 𝑦ᵢ , 𝑦ᵢ 을 사용한다.

또한 판별자의 마지막 층에서 시그모이드 활성화 함수를 제거하여 [0,1] 범위의 출력을 [-∞, ∞]의 범위로 늘린다.

확률이 아닌 점수의 개념을 반환한다.

 

따라서 와서스테인 손실 함수에 따라

 

판별자는 진짜 이미지에 대한 예측 𝛲ᵢ = 𝐷(𝑥ᵢ)와 𝑦ᵢ = 1을 비교하고

생성된 이미지에 대한 예측 𝛲ᵢ = 𝐷(G(zᵢ))와 𝑦ᵢ = -1을 비교하지만,

 

생성자는 진짜 이미지에 대한 예측 𝛲ᵢ = 𝐷(𝑥ᵢ)와 𝑦ᵢ = 1을 비교하여 손실을 계산한다는 점에서 BCE와 차이가 생긴다.

 

 

판별자, 생성자 함수의 최소화는 다음과 정의된다.

판별자 함수 최소화
생성자 함수 최소화

 

이는 판별자는 진짜 이미지와 생성된 이미지에 대한 예측 사이의 차이를 최대화하는 것에 관심이 있으며,

생성자는 판별자로부터 가능한 한 높은 점수를 받는 이미지를 생성하는 것에 관심이 있게 된다.

 

3-2 립시츠 제약

판별자의 마지막 층에 시그모이드 활성 함수를 사용하여 [0,1]의 출력 범위를 가지지 않고

이를 없애 [-∞, ∞]의 출력 범위를 갖게 하는 이유는 뭘까?

일반적으로 신경망에서 손실 함수 및 가중치가 큰 값을 가지는 것은 파라미터 업데이트에 문제가 된다.

따라서 이 큰 값의 출력 범위를 어느 정도 제한해줘야 한다.

 

제한에 앞서 판별자는 1-립시츠 연속 함수를 따라야 한다고 한다.

하나의 이미지를 하나의 예측으로 변환하는 함수가 D라고 했을 때,

임의의 두 입력 이미지 x1, x2에 대해 다음 부등식을 만족할 때 이 함수를 1-립시츠라고 한다.

 

1-립시츠

 

분모의 값은 두 이미지 픽셀의 평균적인 절댓값 차이를 의미하고

분자의 값은 판별자 예측 간의 절댓값 차이를 의미한다.

이미지 사이의 판별자의 예측이 변화하는 비율이 1이어야 함을 의미한다.

 

 

3-3 그레이디언트 페널티 손실

그레이디언트 노름이 1에서 벗어날 경우 모델에 불이익을 주는 그레이디언트 페널티항을 판별자 손실 함수에 포함시켜

립시츠 제약 조건을 직접 강제하게 했다.

그림 4-12를 보면 진짜 이미지와 가짜 이미지에 대한 와서스테인 손실과 더불어 그레이디언트 페널티 손실이 추가되어 

전체의 손실 함수를 구성하는 것을 볼 수 있다.

여기서 그레이디언트 패널티 손실은 입력 이미지에 대한 예측의 그레이디언트 노름과 1 사이의 차이를 제곱한 것이라고 한다.

 

4.  CGAN - conditional GAN

기존의 GAN과 달리 출력을 제어할 수 있는 CGAN에 대해 알아보자.

 

표준 GAN과 CGAN의 주요한 차이점은 CGAN에서 레이블과 관련된 추가 정보를 생성자와 판별자에게 전달한다는 것이다.

생성자에서는 이 정보를 원핫 인코딩 된 벡터로 잠재 공간 샘플에 단순히 추가하고,

판별자에서는 레이블 정보를 RGB 이미지에 추가 채널로 추가한다.

 

그렇다면 CGAN의 출력 제어는 어떻게 동작할까?

특정 원핫 인코딩된 레이블을 생성자의 입력에 전달하면 된다.

예를 들어 금발의 얼굴은 [0,1], 금발이 아닌 얼굴은 [1,0]에 벡터를 전달하면 된다.

이는 GAN이 개별 특성이 서로 분리되도록 잠재공간의 포인트를 구성할 수 있다는 증거가 된다.

아래의 그림 4-17을 보면 이해하기 쉽다.