Ssul's Blog

[Generative-AI] GAN(Generative Adversarial Networks-생성적 적대 신경망)이해 및 구현 본문

AI & ML/개념잡기

[Generative-AI] GAN(Generative Adversarial Networks-생성적 적대 신경망)이해 및 구현

Ssul 2024. 1. 5. 10:22

0. GAN 아이디어

- 이미지 생성자모델과 이미지 판별자 모델 두개를 만들어서, 서로 경쟁하듯 학습

- 생성자(Generator): 임의의 노이즈를 입력 받아 그럴듯한 이미지를 생성하는 기능 학습

- 판별자(Discriminator): 입력된 이미지가 실제 이미지인지, 생성자가 생성한 이미지인지 구분하는 기능 학습

GAN(출처: https://www.analyticsvidhya.com/)

- 생성자는 더 실제같은 이미지를 만드는 신경망을 학습하고,

- 판별자는 입력된 이미지가 진짜 이미지인지, 생성된 이미지인지 판별하는 신경망을 학습한다.

- 이렇게 둘이서 경쟁하듯 학습하면(생성기는 판별기를 속이려하고, 판별기는 생성기를 구분하려 함) 생성자는 정말 실사 같은 이미지를 만들어내게 될수 있다

- 물론 현실에서 판별자가 일찍 학습이 되어, 생성자가 그럴듯한 이미지 자체를 못만들어내는 경우가 많다. 그만큼 GAN학습이 어렵다고 한다.

 

1. GAN 코드로 구현하기

1-1. 데이터 셋팅

IMAGE_SIZE = 64
CHANNELS = 1
BATCH_SIZE = 128
Z_DIM = 100
EPOCHS = 100 # 훈련이 오래 걸려 에포크 횟수를 300에서 100으로 줄입니다.
LOAD_MODEL = False
ADAM_BETA_1 = 0.5
ADAM_BETA_2 = 0.999
LEARNING_RATE = 0.0002
NOISE_PARAM = 0.1


import gdown
gdown.download(id='1qd50QDZtr_NYFiFVdp0sIvGDwTT3mMEQ')
!unzip -q lego-brick-images.zip
# output 디렉토리를 만듭니다.
!mkdir output

train_data = utils.image_dataset_from_directory(
    "/content/dataset/",
    labels=None,
    color_mode="grayscale",
    image_size=(IMAGE_SIZE, IMAGE_SIZE),
    batch_size=BATCH_SIZE,
    shuffle=True,
    seed=42,
    # 이미지를 지정된 image_size로 조정할 때, bilinear알고리즘 사용
    # 4개의 픽셀을 기반으로 새로운 픽셀 값을 계산
    interpolation="bilinear",
)

def preprocess(img):
    """
    이미지 정규화 및 크기 변경
    """
    # -1과 1 사이의 범위로 변환
    # tf.cast(img, "float32") 부분은 이미지의 데이터 타입을 float32로 변환
    img = (tf.cast(img, "float32") - 127.5) / 127.5
    return img


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

train_sample = sample_batch(train)
display(train_sample)

- 레고블럭 이미지 데이터를 가져옵니다.

- 아래와 같은 이미지 입니다.

 

 

1-2. GAN모델 구성(Discriminator)

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)
# (4,4,512) > (1,1,1)개로해서 sigmoid > 0 or 1
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()

- 이미지를 입력 받는다.

- 이미지를 CNN신경망을 통과시켜, 최종적으로 1개의 노드로 모음. 이를 sigmoid를 통과시켜 0 또는 1값을 가지게 함

- 0이면 생성자가 생성한 가짜 이미지, 1이면 진짜 이미지

 

1-3. GAN모델 구성(Generator)

# 잠재공간의 차원 Z_DIM = 100차원
generator_input = layers.Input(shape=(Z_DIM,))
# (1,1,100)
x = layers.Reshape((1, 1, Z_DIM))(generator_input)
# (4,4,512)
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()

- 100차원의 임의의 벡터가 입력

- 임의의 이미지 벡터가 생성(output)하는 신경망 모델

 

1-4. GAN모델 구성(Generator + Disciriminator)

class DCGAN(models.Model):
    def __init__(self, discriminator, generator, latent_dim):
        super(DCGAN, self).__init__()
        self.discriminator = discriminator
        self.generator = generator
        self.latent_dim = latent_dim

    def compile(self, d_optimizer, g_optimizer):
        super(DCGAN, self).compile()
        self.loss_fn = losses.BinaryCrossentropy()
        self.d_optimizer = d_optimizer
        self.g_optimizer = g_optimizer
        self.d_loss_metric = metrics.Mean(name="d_loss")
        self.d_real_acc_metric = metrics.BinaryAccuracy(name="d_real_acc")
        self.d_fake_acc_metric = metrics.BinaryAccuracy(name="d_fake_acc")
        self.d_acc_metric = metrics.BinaryAccuracy(name="d_acc")
        self.g_loss_metric = metrics.Mean(name="g_loss")
        self.g_acc_metric = metrics.BinaryAccuracy(name="g_acc")

    @property
    def metrics(self):
        return [
            self.d_loss_metric,
            self.d_real_acc_metric,
            self.d_fake_acc_metric,
            self.d_acc_metric,
            self.g_loss_metric,
            self.g_acc_metric,
        ]

    def train_step(self, real_images):
        # 잠재 공간에서 랜덤 포인트 샘플링
        batch_size = tf.shape(real_images)[0]
        # 생성자에 입력할 랜덤 latent 벡터 생성
        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
            )

            # 진짜 이미지에 대한 y값은 1로
            real_labels = tf.ones_like(real_predictions)
            real_noisy_labels = real_labels + NOISE_PARAM * tf.random.uniform(
                tf.shape(real_predictions)
            )
            # 가짜 이미지에 대한 y값은 0으로
            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

            # 생성자의 로스함수: 생성기가 생산한 이미지의 예측이 1에 가까워지게
            # 최대한 진짜같이 만들기
            g_loss = self.loss_fn(real_labels, fake_predictions)

        # loss_fn, 파라미터 주고, 업데이트
        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 생성
dcgan = DCGAN(
    discriminator=discriminator, generator=generator, latent_dim=Z_DIM
)

- random_latent_vectors = tf.random.normal(shape=(batch_size, self.latent_dim)) : 생성된 임의의 벡터. 해당 벡터를 generator에 넣어서 가짜 이미지를 생성하고, 해당 벡터의 label은 0으로.

- 진짜 이미지 넣었을때 discriminator의 예측값과 1로 loss_fun구성

- 가짜 이미지 넣었을때 discriminator의 예측값과 0으로 loss_fun구성

d_loss = (d_real_loss + d_fake_loss) / 2.0

- 판별자의 두개의 loss함수의 평균 > 이 loss함수를 줄이는 방향이, 가짜/진짜를 잘 찾아내는 것

 

g_loss = self.loss_fn(real_labels, fake_predictions)

- 반대로 생성자는 진짜 이미지라벨과 가짜이미지의 예측값을 loss_fun로 하여, 이를 최소화 하는 방향으로 학습

 

 

1-5. GAN훈련

dcgan.compile(
    d_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
    ),
)

# 모델 저장 체크포인트 만들기
model_checkpoint_callback = callbacks.ModelCheckpoint(
    filepath="/content/checkpoint/checkpoint.ckpt",
    save_weights_only=True,
    save_freq="epoch",
    verbose=0,
)

tensorboard_callback = callbacks.TensorBoard(log_dir="./logs")


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: # 출력 횟수를 줄이기 위해
            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="/content/output/generated_img_%03d.png" % (epoch),
            )
            
dcgan.fit(
    train,
    epochs=EPOCHS,
    callbacks=[
        model_checkpoint_callback,
        tensorboard_callback,
        ImageGenerator(num_img=10, latent_dim=Z_DIM),
    ],
)

- 학습 중간 중간에 generator가 생성한 이미지를 출력합니다.

- 완벽하지는 않아도, 학습이 진행됨에 따라, 약간은 형태를 갖춘 이미지를 생성하는 것을 볼수 있음

생성된 이미지와 원래이미지가 약간은 유사한 모양을 가지고 있음을 확인할 수 있다.

 

2. GAN정리

- GAN은 재미있는 원리이지만, 훈련이 어렵기로 유명함

- 판별자가 생성자보다 크게 뛰어나거나, 생성자가 판별자보다 크게 뛰어난 경우 학습이 안됨(손실이 유용하지 않음)

- 이런 학습의 안정성과 품질을 개선한 WGAN-GP, CGAN 등이 있다.

- 또한, 더 발전된 고급 GAN(ProGAN, SAGAN, ViT VQ-GAN 등)도 있다고 한다