🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

GAN으로 이미지 생성 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 2. · 12 Views

GAN으로 이미지 생성 완벽 가이드

생성적 적대 신경망(GAN)의 핵심 개념부터 DCGAN 구현까지, 초급 개발자도 이해할 수 있도록 쉽게 설명합니다. 실제 작동하는 PyTorch 코드와 함께 이미지 생성 AI의 세계로 안내합니다.


목차

  1. 생성적_적대_신경망_GAN_개념
  2. Generator와_Discriminator
  3. 손실_함수_설계
  4. DCGAN_구현
  5. 학습_안정화_기법
  6. 생성된_이미지_평가

1. 생성적 적대 신경망 GAN 개념

어느 날 김개발 씨가 인공지능 관련 뉴스를 보다가 깜짝 놀랐습니다. "이 사진 속 사람이 실존하지 않는다고요?" AI가 만들어낸 가짜 얼굴이 진짜보다 더 진짜 같아 보였기 때문입니다.

도대체 어떻게 컴퓨터가 이런 이미지를 만들어낼 수 있는 걸까요?

**GAN(Generative Adversarial Network)**은 한마디로 두 개의 신경망이 서로 경쟁하며 학습하는 구조입니다. 마치 위조지폐범과 경찰이 서로 실력을 겨루듯이, 하나는 가짜를 만들고 다른 하나는 가짜를 찾아냅니다.

이 경쟁이 반복될수록 가짜를 만드는 쪽의 실력이 점점 좋아져서 결국 진짜와 구별할 수 없는 이미지를 생성하게 됩니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

# GAN의 기본 구조: Generator와 Discriminator
class SimpleGAN:
    def __init__(self):
        # Generator: 랜덤 노이즈를 이미지로 변환
        self.generator = self.build_generator()
        # Discriminator: 진짜와 가짜를 판별
        self.discriminator = self.build_discriminator()

    def train_step(self, real_images):
        # 1단계: Discriminator 학습 - 진짜와 가짜 구별하기
        fake_images = self.generator(torch.randn(batch_size, 100))
        # 2단계: Generator 학습 - Discriminator 속이기
        return fake_images

김개발 씨는 입사 6개월 차 주니어 개발자입니다. 최근 회사에서 AI 이미지 생성 프로젝트를 시작한다는 소식을 들었습니다.

팀장님이 "GAN 기술을 사용할 거야"라고 말씀하셨는데, 김개발 씨는 GAN이 무엇인지 전혀 몰랐습니다. 점심시간에 선배 개발자 박시니어 씨에게 조심스럽게 물었습니다.

"선배님, GAN이 뭔가요? 이름만 들어봤는데 어떻게 작동하는지 모르겠어요." 박시니어 씨가 웃으며 설명을 시작했습니다.

"GAN을 이해하려면 먼저 재미있는 비유를 하나 들려줄게요." "옛날에 아주 뛰어난 위조지폐범이 있었다고 상상해 봐요. 이 위조지폐범은 처음에는 실력이 형편없어서 만든 지폐가 바로 들통났어요.

하지만 경찰이 어떤 점을 보고 가짜를 판별하는지 계속 피드백을 받으면서 실력이 늘었죠." "동시에 경찰도 점점 더 정교해지는 위조지폐를 구별하기 위해 실력을 키워야 했어요. 이렇게 둘이 서로 경쟁하다 보니, 위조지폐범의 실력은 하늘을 찔렀고, 경찰의 판별 능력도 극도로 정밀해졌죠." 김개발 씨가 고개를 끄덕였습니다.

"아, 그러니까 서로 경쟁하면서 둘 다 실력이 늘어난다는 거군요!" "맞아요. GAN에서 위조지폐범 역할을 하는 게 **Generator(생성자)**이고, 경찰 역할을 하는 게 **Discriminator(판별자)**예요.

Generator는 랜덤 노이즈를 입력받아서 가짜 이미지를 만들어내고, Discriminator는 이 이미지가 진짜인지 가짜인지 판별하죠." 이 개념은 2014년 이안 굿펠로우(Ian Goodfellow)가 처음 제안했습니다. 당시 학계에서는 "이렇게 간단한 아이디어가 이렇게 강력할 수 있다니"라며 놀라워했습니다.

GAN이 등장하기 전에는 이미지를 생성하기가 매우 어려웠습니다. 픽셀 하나하나의 확률 분포를 직접 모델링해야 했는데, 이는 계산량도 많고 결과물의 품질도 좋지 않았습니다.

하지만 GAN은 확률 분포를 직접 계산하지 않습니다. 대신 "진짜 같은지 아닌지"만 판단하면 됩니다.

이 단순한 접근 방식이 놀라운 결과를 만들어냈습니다. 실제 현업에서 GAN은 다양한 곳에 활용됩니다.

게임 회사에서는 배경 이미지를 자동 생성하고, 패션 업계에서는 새로운 디자인을 만들어냅니다. 의료 분야에서는 희귀 질환의 의료 영상 데이터를 늘리는 데 사용하기도 합니다.

다만 주의할 점도 있습니다. GAN은 학습이 불안정하기로 유명합니다.

Generator와 Discriminator의 실력 차이가 너무 벌어지면 학습이 제대로 이루어지지 않습니다. 이 문제는 뒤에서 더 자세히 다루겠습니다.

박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝였습니다. "정말 재미있는 아이디어네요!

경쟁을 통해 학습한다니, 마치 게임 같아요."

실전 팁

💡 - GAN의 핵심은 경쟁 학습입니다. Generator와 Discriminator가 균형 있게 발전해야 좋은 결과를 얻습니다

  • 처음에는 간단한 데이터셋(MNIST 등)으로 시작하세요. 복잡한 이미지는 학습이 훨씬 어렵습니다

2. Generator와 Discriminator

김개발 씨가 GAN의 기본 개념을 이해한 후, 다음 질문이 떠올랐습니다. "그런데 Generator와 Discriminator는 구체적으로 어떻게 생겼나요?" 박시니어 씨가 화이트보드를 꺼내며 본격적인 설명을 시작했습니다.

Generator는 랜덤 노이즈 벡터를 입력받아 이미지를 출력하는 신경망입니다. 마치 화가가 영감(노이즈)을 받아 그림을 그리는 것과 같습니다.

Discriminator는 이미지를 입력받아 진짜일 확률을 출력하는 신경망입니다. 감정사가 그림의 진위를 판별하는 것과 같은 역할을 합니다.

다음 코드를 살펴봅시다.

import torch.nn as nn

# Generator: 노이즈를 이미지로 변환
class Generator(nn.Module):
    def __init__(self, latent_dim=100, img_shape=784):
        super().__init__()
        self.model = nn.Sequential(
            nn.Linear(latent_dim, 256),   # 100차원 노이즈를 256차원으로 확장
            nn.LeakyReLU(0.2),
            nn.Linear(256, 512),           # 점점 더 복잡한 특징 학습
            nn.LeakyReLU(0.2),
            nn.Linear(512, img_shape),     # 최종 이미지 크기로 출력
            nn.Tanh()                      # 픽셀 값을 -1~1 범위로 정규화
        )

    def forward(self, z):
        return self.model(z)

# Discriminator: 이미지의 진위 판별
class Discriminator(nn.Module):
    def __init__(self, img_shape=784):
        super().__init__()
        self.model = nn.Sequential(
            nn.Linear(img_shape, 512),
            nn.LeakyReLU(0.2),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2),
            nn.Linear(256, 1),
            nn.Sigmoid()                   # 0~1 사이의 확률 출력
        )

    def forward(self, img):
        return self.model(img)

박시니어 씨가 화이트보드에 두 개의 상자를 그렸습니다. 왼쪽에는 "Generator", 오른쪽에는 "Discriminator"라고 적었습니다.

"먼저 Generator를 설명할게요. Generator는 마치 미술 학원에 다니는 학생과 같아요.

처음에는 그림 실력이 형편없지만, 선생님의 피드백을 받으면서 점점 나아지죠." Generator가 입력으로 받는 것은 **잠재 벡터(latent vector)**라고 불리는 랜덤 노이즈입니다. 보통 100차원 정도의 무작위 숫자들이죠.

이 숫자들이 신경망을 통과하면서 점점 이미지의 형태를 갖춰갑니다. "왜 랜덤 노이즈를 사용하나요?" 김개발 씨가 물었습니다.

"좋은 질문이에요. 노이즈가 다르면 다른 이미지가 생성되거든요.

만약 항상 같은 입력을 주면 항상 같은 이미지만 나오겠죠? 다양한 노이즈를 주면 다양한 이미지를 만들 수 있어요." 코드를 보면 Generator는 Linear 레이어와 LeakyReLU 활성화 함수로 구성됩니다.

100차원의 노이즈가 256, 512 차원을 거쳐 최종적으로 784차원(28x28 이미지)의 출력이 됩니다. 마지막에 Tanh 함수를 사용하는 이유는 출력 값을 -1에서 1 사이로 맞추기 위해서입니다.

이제 Discriminator 차례입니다. Discriminator는 이미지를 입력받아 "이 이미지가 진짜일 확률"을 출력합니다.

"Discriminator는 미술품 감정사와 같아요. 진품과 위작을 구별하는 능력을 키워야 하죠.

처음에는 구별을 못 하지만, 많은 작품을 보면서 눈이 높아져요." Discriminator의 구조는 Generator와 반대입니다. 이미지(784차원)를 입력받아 점점 차원을 줄여가며, 마지막에 Sigmoid 함수를 통해 0에서 1 사이의 확률 값을 출력합니다.

1에 가까우면 "진짜", 0에 가까우면 "가짜"라고 판단하는 것입니다. 여기서 중요한 점이 있습니다.

LeakyReLU를 사용하는 이유입니다. 일반 ReLU는 음수 입력에 대해 0을 출력하는데, 이렇게 되면 기울기가 사라져서 학습이 잘 안 됩니다.

LeakyReLU는 음수에도 작은 기울기를 유지해서 이 문제를 해결합니다. 실무에서는 이렇게 단순한 Linear 레이어 대신 Convolutional 레이어를 많이 사용합니다.

이미지의 공간적 특징을 더 잘 잡아낼 수 있기 때문입니다. 이건 다음 카드에서 DCGAN을 다룰 때 자세히 설명하겠습니다.

김개발 씨가 코드를 유심히 살펴보다가 물었습니다. "두 네트워크의 구조가 비슷해 보이는데, 왜 반대 방향으로 설계하나요?" 박시니어 씨가 고개를 끄덕였습니다.

"Generator는 작은 정보에서 큰 이미지를 만들어야 하니까 점점 커지고, Discriminator는 큰 이미지에서 하나의 판단을 내려야 하니까 점점 작아지는 거예요. 목적에 맞게 설계한 거죠."

실전 팁

💡 - Generator의 마지막 활성화 함수는 보통 Tanh를 사용합니다. 이미지 픽셀 값의 범위와 맞춰주기 위함입니다

  • LeakyReLU의 기울기는 보통 0.2를 사용합니다. 너무 작으면 효과가 없고, 너무 크면 일반 ReLU와 차이가 없습니다

3. 손실 함수 설계

김개발 씨가 Generator와 Discriminator의 구조를 이해했습니다. 하지만 한 가지 의문이 남았습니다.

"이 두 네트워크가 어떻게 서로 경쟁하면서 학습하는 건가요?" 핵심은 바로 손실 함수에 있었습니다.

GAN의 손실 함수는 **미니맥스 게임(minimax game)**으로 설계됩니다. Discriminator는 진짜를 진짜로, 가짜를 가짜로 판별하면 보상을 받습니다.

Generator는 Discriminator를 속이면 보상을 받습니다. 이 두 목표가 서로 반대되기 때문에 "적대적(adversarial)"이라는 이름이 붙었습니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

# Binary Cross Entropy 손실 함수
criterion = nn.BCELoss()

def train_discriminator(real_images, generator, discriminator, optimizer_d):
    batch_size = real_images.size(0)
    real_labels = torch.ones(batch_size, 1)   # 진짜 이미지 레이블: 1
    fake_labels = torch.zeros(batch_size, 1)  # 가짜 이미지 레이블: 0

    # 진짜 이미지에 대한 손실
    real_output = discriminator(real_images)
    loss_real = criterion(real_output, real_labels)

    # 가짜 이미지에 대한 손실
    z = torch.randn(batch_size, 100)
    fake_images = generator(z)
    fake_output = discriminator(fake_images.detach())
    loss_fake = criterion(fake_output, fake_labels)

    # 총 Discriminator 손실
    loss_d = loss_real + loss_fake
    return loss_d

def train_generator(generator, discriminator, optimizer_g, batch_size):
    z = torch.randn(batch_size, 100)
    fake_images = generator(z)
    output = discriminator(fake_images)
    # Generator는 Discriminator가 1을 출력하길 원함
    loss_g = criterion(output, torch.ones(batch_size, 1))
    return loss_g

박시니어 씨가 새로운 주제를 꺼냈습니다. "이제 가장 중요한 부분이에요.

손실 함수를 어떻게 설계하느냐가 GAN 학습의 핵심이거든요." 손실 함수란 모델이 얼마나 잘하고 있는지를 측정하는 척도입니다. 신경망은 이 손실을 줄이는 방향으로 가중치를 업데이트합니다.

"GAN에서는 두 개의 손실 함수가 필요해요. Discriminator용 하나, Generator용 하나." 먼저 Discriminator의 손실 함수를 살펴봅시다.

Discriminator의 목표는 단순합니다. 진짜 이미지가 들어오면 1에 가까운 값을, 가짜 이미지가 들어오면 0에 가까운 값을 출력하는 것입니다.

코드에서 real_labels는 모두 1이고, fake_labels는 모두 0입니다. Discriminator가 진짜 이미지에 대해 1을 출력하면 loss_real이 작아지고, 가짜 이미지에 대해 0을 출력하면 loss_fake가 작아집니다.

김개발 씨가 질문했습니다. "detach()는 왜 사용하나요?" "좋은 질문이에요!

Discriminator를 학습할 때는 Generator의 가중치를 건드리면 안 돼요. detach()를 호출하면 fake_images가 Generator와의 연결이 끊어져서, 역전파 시 Generator까지 기울기가 전달되지 않아요." 이제 Generator의 손실 함수입니다.

Generator의 목표는 Discriminator를 속이는 것입니다. 즉, 가짜 이미지를 넣었을 때 Discriminator가 1을 출력하게 만들어야 합니다.

코드를 보면 Generator의 손실을 계산할 때 레이블로 torch.ones를 사용합니다. Generator 입장에서는 자신이 만든 가짜 이미지가 진짜로 판별받아야 하니까요.

이것이 바로 미니맥스 게임입니다. Discriminator는 "가짜를 찾아내겠다"며 손실을 최소화하려 하고, Generator는 "Discriminator를 속이겠다"며 반대 방향으로 움직입니다.

수학적으로 표현하면 이렇습니다: min_G max_D V(D, G). Discriminator는 V를 최대화하려 하고, Generator는 V를 최소화하려 합니다.

실제 학습에서는 한 가지 트릭을 사용합니다. Generator의 손실을 계산할 때 log(1-D(G(z)))를 최소화하는 대신, log(D(G(z)))를 최대화합니다.

수학적으로는 같은 방향이지만, 후자가 학습 초기에 더 강한 기울기를 제공해서 학습이 잘 됩니다. "이해했어요!

결국 두 네트워크가 서로 다른 목표를 향해 경쟁하는 거군요." 김개발 씨가 정리했습니다. 박시니어 씨가 덧붙였습니다.

"맞아요. 그리고 이 경쟁이 균형 있게 진행되어야 해요.

한쪽이 너무 강해지면 학습이 망가지거든요. 이 문제는 나중에 학습 안정화 기법에서 다룰 거예요."

실전 팁

💡 - Discriminator와 Generator의 학습 비율을 조절하세요. 보통 Discriminator를 먼저 k번 학습시킨 후 Generator를 1번 학습시킵니다

  • 학습 초기에는 Generator의 손실이 매우 높은 것이 정상입니다. Discriminator가 쉽게 구별할 수 있으니까요

4. DCGAN 구현

김개발 씨가 기본적인 GAN 구조를 이해한 후, 박시니어 씨가 새로운 도전을 제안했습니다. "이제 진짜 이미지다운 이미지를 만들어볼까요?" 단순한 Linear 레이어로는 한계가 있었습니다.

바로 여기서 DCGAN이 등장합니다.

**DCGAN(Deep Convolutional GAN)**은 합성곱 신경망을 활용한 GAN입니다. Linear 레이어 대신 Conv2d와 ConvTranspose2d를 사용해서 이미지의 공간적 특징을 더 잘 잡아냅니다.

마치 화가가 붓질의 결을 살려 그림을 그리듯이, DCGAN은 이미지의 질감과 패턴을 자연스럽게 생성합니다.

다음 코드를 살펴봅시다.

import torch.nn as nn

class DCGANGenerator(nn.Module):
    def __init__(self, latent_dim=100, feature_maps=64):
        super().__init__()
        self.model = nn.Sequential(
            # 입력: 100차원 노이즈, 출력: 512 x 4 x 4
            nn.ConvTranspose2d(latent_dim, feature_maps*8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(feature_maps*8),
            nn.ReLU(True),
            # 출력: 256 x 8 x 8
            nn.ConvTranspose2d(feature_maps*8, feature_maps*4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(feature_maps*4),
            nn.ReLU(True),
            # 출력: 128 x 16 x 16
            nn.ConvTranspose2d(feature_maps*4, feature_maps*2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(feature_maps*2),
            nn.ReLU(True),
            # 출력: 64 x 32 x 32
            nn.ConvTranspose2d(feature_maps*2, feature_maps, 4, 2, 1, bias=False),
            nn.BatchNorm2d(feature_maps),
            nn.ReLU(True),
            # 최종 출력: 3 x 64 x 64 (RGB 이미지)
            nn.ConvTranspose2d(feature_maps, 3, 4, 2, 1, bias=False),
            nn.Tanh()
        )

    def forward(self, z):
        return self.model(z.view(-1, 100, 1, 1))

class DCGANDiscriminator(nn.Module):
    def __init__(self, feature_maps=64):
        super().__init__()
        self.model = nn.Sequential(
            # 입력: 3 x 64 x 64
            nn.Conv2d(3, feature_maps, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # 출력: 128 x 16 x 16
            nn.Conv2d(feature_maps, feature_maps*2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(feature_maps*2),
            nn.LeakyReLU(0.2, inplace=True),
            # 최종 판별
            nn.Conv2d(feature_maps*2, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

박시니어 씨가 노트북을 열며 말했습니다. "지금까지 배운 GAN은 기초 버전이에요.

실제로 좋은 이미지를 생성하려면 DCGAN을 사용해야 해요." "DC가 무슨 뜻인가요?" 김개발 씨가 물었습니다. "Deep Convolutional의 약자예요.

합성곱(Convolution) 연산을 사용한다는 뜻이죠." 합성곱이 왜 중요한지 이해하려면 이미지의 특성을 생각해봐야 합니다. 이미지에서 인접한 픽셀들은 서로 관련이 있습니다.

예를 들어 하늘 사진에서 파란색 픽셀 옆에는 대부분 파란색 픽셀이 있죠. 합성곱은 이런 공간적 관계를 잘 포착합니다.

Generator에서 사용하는 ConvTranspose2d는 "역합성곱"이라고도 불립니다. 작은 특징 맵을 큰 이미지로 확장하는 역할을 합니다.

마치 작은 스케치에서 큰 그림을 그려나가는 것과 같습니다. 코드를 보면 Generator는 100차원의 노이즈에서 시작해서 4x4, 8x8, 16x16, 32x32를 거쳐 최종 64x64 크기의 이미지를 생성합니다.

각 단계에서 해상도가 2배씩 늘어나는 것을 볼 수 있습니다. DCGAN의 핵심 비법이 몇 가지 있습니다.

첫째, BatchNorm을 사용합니다. BatchNorm은 각 레이어의 출력을 정규화해서 학습을 안정화시킵니다.

다만 Generator의 마지막 레이어와 Discriminator의 첫 레이어에는 사용하지 않습니다. 둘째, 활성화 함수 선택이 중요합니다.

Generator에서는 ReLU를, Discriminator에서는 LeakyReLU를 사용합니다. 이 조합이 경험적으로 가장 좋은 결과를 내는 것으로 알려져 있습니다.

셋째, 풀링 레이어를 사용하지 않습니다. 대신 stride가 2인 합성곱으로 다운샘플링을 합니다.

풀링은 정보를 버리는 반면, 학습 가능한 다운샘플링은 중요한 정보를 보존하면서 크기를 줄일 수 있습니다. Discriminator는 Generator와 반대 구조입니다.

64x64 이미지를 입력받아 점점 작은 특징 맵으로 줄여가며, 최종적으로 하나의 확률 값을 출력합니다. "ConvTranspose2d의 파라미터가 좀 헷갈려요." 김개발 씨가 고백했습니다.

박시니어 씨가 친절히 설명했습니다. "순서대로 입력 채널, 출력 채널, 커널 크기, stride, 패딩이에요.

stride가 2면 출력 크기가 2배가 되고, 패딩 1은 경계 처리를 해주죠. 처음에는 외우려 하지 말고 출력 크기가 어떻게 변하는지만 이해하면 돼요." 실무에서 DCGAN을 사용할 때는 이미지 크기에 맞게 레이어 수를 조절해야 합니다.

128x128 이미지를 생성하려면 레이어를 하나 더 추가하면 됩니다.

실전 팁

💡 - ConvTranspose2d에서 체커보드 아티팩트가 생기면 커널 크기를 stride로 나눌 수 있는 값으로 설정하세요

  • 학습률은 보통 0.0002로 설정하고, Adam 옵티마이저의 beta1은 0.5로 설정합니다

5. 학습 안정화 기법

김개발 씨가 DCGAN을 직접 구현해서 돌려봤습니다. 그런데 결과가 영 신통치 않았습니다.

어떤 때는 이상한 노이즈만 나오고, 어떤 때는 모든 이미지가 똑같이 생겼습니다. "선배님, 뭐가 잘못된 걸까요?" 박시니어 씨가 웃으며 말했습니다.

"GAN 학습이 원래 그래요. 안정화 기법을 알려줄게요."

GAN 학습의 대표적인 문제는 **모드 붕괴(mode collapse)**와 학습 불안정입니다. 모드 붕괴는 Generator가 다양한 이미지 대신 비슷한 이미지만 생성하는 현상입니다.

이를 해결하기 위해 라벨 스무딩, 노이즈 추가, 스펙트럴 정규화 등의 기법을 사용합니다.

다음 코드를 살펴봅시다.

import torch
import torch.nn as nn

# 1. 라벨 스무딩: 1 대신 0.9, 0 대신 0.1 사용
def smooth_labels(real_labels, fake_labels):
    real_labels = real_labels * 0.9  # 1 -> 0.9
    fake_labels = fake_labels + 0.1  # 0 -> 0.1
    return real_labels, fake_labels

# 2. 입력에 노이즈 추가
def add_instance_noise(images, std=0.1):
    noise = torch.randn_like(images) * std
    return images + noise

# 3. 스펙트럴 정규화 적용
from torch.nn.utils import spectral_norm

class StableDiscriminator(nn.Module):
    def __init__(self):
        super().__init__()
        self.model = nn.Sequential(
            spectral_norm(nn.Conv2d(3, 64, 4, 2, 1)),
            nn.LeakyReLU(0.2),
            spectral_norm(nn.Conv2d(64, 128, 4, 2, 1)),
            nn.LeakyReLU(0.2),
        )

# 4. 학습률 조절: Two Time-Scale Update Rule
optimizer_g = torch.optim.Adam(generator.parameters(), lr=0.0001, betas=(0.5, 0.999))
optimizer_d = torch.optim.Adam(discriminator.parameters(), lr=0.0004, betas=(0.5, 0.999))

박시니어 씨가 화이트보드에 그래프를 그리며 설명을 시작했습니다. "GAN 학습이 어려운 이유가 몇 가지 있어요." 첫 번째 문제는 **모드 붕괴(mode collapse)**입니다.

이건 Generator가 다양한 이미지를 만들지 못하고, 몇 가지 패턴만 반복해서 만드는 현상입니다. "왜 이런 일이 생기나요?" 김개발 씨가 물었습니다.

"Generator 입장에서 생각해보세요. Discriminator를 속이는 게 목표잖아요.

어떤 특정 패턴이 Discriminator를 잘 속인다면, Generator는 계속 그 패턴만 만들려고 할 거예요. 굳이 다른 걸 시도할 이유가 없으니까요." 이 문제를 해결하는 첫 번째 방법은 라벨 스무딩입니다.

진짜 이미지의 레이블을 1 대신 0.9로, 가짜 이미지의 레이블을 0 대신 0.1로 설정합니다. 이렇게 하면 Discriminator가 너무 확신에 차서 과적합되는 것을 방지할 수 있습니다.

두 번째 방법은 입력에 노이즈를 추가하는 것입니다. 진짜 이미지와 가짜 이미지 모두에 약간의 노이즈를 더해서 Discriminator의 판별을 어렵게 만듭니다.

학습이 진행되면서 노이즈의 크기를 점점 줄여나갑니다. 세 번째 방법은 **스펙트럴 정규화(spectral normalization)**입니다.

이건 Discriminator의 가중치를 정규화해서 Lipschitz 연속성을 보장하는 기법입니다. "Lipschitz 연속성이 뭔가요?" 김개발 씨가 어려운 용어에 당황했습니다.

"쉽게 말하면, Discriminator의 출력이 입력의 작은 변화에 너무 민감하게 반응하지 않도록 하는 거예요. 안정적인 기울기를 제공해서 학습이 잘 되도록 돕죠." 네 번째 방법은 **Two Time-Scale Update Rule(TTUR)**입니다.

Discriminator와 Generator의 학습률을 다르게 설정하는 것입니다. 보통 Discriminator의 학습률을 더 높게 설정합니다.

코드에서 보면 Discriminator는 0.0004, Generator는 0.0001로 설정했습니다. "왜 Discriminator를 더 빨리 학습시키나요?" "Discriminator가 더 빨리 좋아져야 Generator에게 유용한 피드백을 줄 수 있거든요.

Discriminator가 너무 약하면 Generator가 어떻게 개선해야 할지 모르게 돼요." 그 외에도 몇 가지 실용적인 팁이 있습니다. 먼저 배치 정규화를 Generator와 Discriminator 모두에 적용하되, Discriminator의 첫 레이어와 Generator의 마지막 레이어에는 적용하지 않습니다.

또한 학습 중에 진짜와 가짜를 섞어서 학습시키는 것이 좋습니다. 배치 전체가 진짜이거나 전체가 가짜인 것보다, 섞여 있는 것이 더 안정적인 학습을 유도합니다.

마지막으로 체크포인트를 자주 저장하세요. GAN 학습은 언제 터질지 모릅니다.

좋은 결과가 나왔을 때 저장해두지 않으면 다시 그 상태로 돌아가기 어렵습니다. 김개발 씨가 고개를 끄덕였습니다.

"GAN 학습이 이렇게 까다로운 줄 몰랐어요. 그래도 방법이 있으니 다행이네요."

실전 팁

💡 - 학습 초기에는 Discriminator가 너무 강해지지 않도록 Generator를 먼저 몇 번 학습시키는 것도 방법입니다

  • 손실 값만 보지 말고 생성된 이미지를 주기적으로 확인하세요. 손실이 좋아 보여도 이미지가 이상할 수 있습니다

6. 생성된 이미지 평가

김개발 씨가 GAN을 열심히 학습시켜서 드디어 그럴듯한 이미지가 나오기 시작했습니다. 그런데 문득 의문이 들었습니다.

"이 이미지가 정말 잘 생성된 건지 어떻게 알 수 있죠?" 사람 눈으로 보기에는 괜찮아 보이는데, 객관적인 평가 방법은 없을까요?

GAN이 생성한 이미지의 품질을 평가하는 대표적인 지표로 **FID(Frechet Inception Distance)**와 **IS(Inception Score)**가 있습니다. FID는 생성된 이미지와 실제 이미지의 분포 차이를 측정하고, IS는 생성된 이미지의 다양성과 품질을 동시에 평가합니다.

낮은 FID와 높은 IS가 좋은 GAN을 의미합니다.

다음 코드를 살펴봅시다.

import torch
import numpy as np
from scipy import linalg
from torchvision.models import inception_v3

def calculate_fid(real_features, fake_features):
    # 실제 이미지와 생성 이미지의 특징 통계 계산
    mu_real = np.mean(real_features, axis=0)
    mu_fake = np.mean(fake_features, axis=0)
    sigma_real = np.cov(real_features, rowvar=False)
    sigma_fake = np.cov(fake_features, rowvar=False)

    # 평균 차이
    diff = mu_real - mu_fake

    # 공분산 행렬의 제곱근 계산
    covmean = linalg.sqrtm(sigma_real @ sigma_fake)
    if np.iscomplexobj(covmean):
        covmean = covmean.real

    # FID 점수 계산: 낮을수록 좋음
    fid = diff @ diff + np.trace(sigma_real + sigma_fake - 2*covmean)
    return fid

def calculate_inception_score(generated_images, inception_model, splits=10):
    # Inception 모델로 클래스 확률 예측
    preds = inception_model(generated_images)
    preds = torch.softmax(preds, dim=1).numpy()

    # IS 계산: p(y|x)와 p(y)의 KL divergence
    split_scores = []
    for k in range(splits):
        part = preds[k * (len(preds) // splits): (k+1) * (len(preds) // splits)]
        py = np.mean(part, axis=0)
        scores = [np.sum(p * np.log(p / py + 1e-10)) for p in part]
        split_scores.append(np.exp(np.mean(scores)))

    return np.mean(split_scores), np.std(split_scores)

박시니어 씨가 마지막 주제를 꺼냈습니다. "GAN을 학습시켰으면 평가도 해야겠죠?

그런데 이미지 품질을 어떻게 수치로 측정할 수 있을까요?" 이건 생각보다 어려운 문제입니다. 이미지의 "좋음"이라는 건 주관적인 개념이니까요.

하지만 연구자들이 몇 가지 유용한 지표를 개발했습니다. 첫 번째는 **Inception Score(IS)**입니다.

이 지표는 두 가지를 측정합니다. 첫째, 품질입니다.

좋은 이미지는 무엇인가 분명해야 합니다. Inception 네트워크에 이미지를 넣었을 때, 특정 클래스에 높은 확률을 주면 그 이미지가 뚜렷하다는 의미입니다.

둘째, 다양성입니다. 생성된 이미지들이 다양해야 합니다.

모든 이미지가 같은 클래스로 분류되면 다양성이 낮은 것입니다. "그런데 Inception 네트워크가 뭔가요?" 김개발 씨가 물었습니다.

"ImageNet으로 학습된 분류 모델이에요. 이미지를 보고 1000개 클래스 중 어떤 건지 맞추는 네트워크죠.

이 네트워크의 판단을 빌려서 생성 이미지의 품질을 평가하는 거예요." IS의 문제점은 실제 데이터 분포를 고려하지 않는다는 것입니다. 아무리 선명한 이미지를 만들어도, 실제 학습 데이터와 전혀 다르면 좋은 GAN이라고 할 수 없죠.

이 문제를 해결하는 것이 **FID(Frechet Inception Distance)**입니다. FID는 생성된 이미지와 실제 이미지의 분포가 얼마나 비슷한지를 측정합니다.

구체적으로, Inception 네트워크의 중간 레이어에서 특징을 추출합니다. 그리고 실제 이미지들의 특징 분포와 생성된 이미지들의 특징 분포를 비교합니다.

두 분포가 비슷할수록 FID가 낮아집니다. 코드를 보면 FID 계산 과정이 꽤 복잡해 보입니다.

평균 벡터의 차이와 공분산 행렬의 차이를 모두 고려하기 때문입니다. 하지만 다행히 pytorch-fid 같은 라이브러리를 사용하면 간단히 계산할 수 있습니다.

"FID와 IS 중에 뭐가 더 중요한가요?" 박시니어 씨가 대답했습니다. "요즘은 FID를 더 많이 사용해요.

실제 데이터와의 유사성을 직접 측정하니까 더 믿을만하다고 여겨지거든요. 논문에서도 FID 점수를 주로 보고하죠." 실무에서 평가할 때 몇 가지 주의점이 있습니다.

첫째, 충분한 양의 이미지(보통 10,000장 이상)로 평가해야 통계적으로 의미가 있습니다. 둘째, 같은 데이터셋에서 계산한 FID끼리 비교해야 합니다.

서로 다른 데이터셋의 FID는 비교할 수 없습니다. 그 외에도 사람이 직접 평가하는 방법도 있습니다.

Amazon Mechanical Turk 같은 플랫폼을 통해 사람들에게 "어떤 이미지가 더 진짜 같나요?"라고 물어보는 것입니다. 하지만 비용이 많이 들고 재현성이 떨어진다는 단점이 있습니다.

김개발 씨가 정리했습니다. "결국 FID를 주로 보면 되는 거군요.

낮을수록 좋고요." "맞아요. 그리고 평가 지표만 믿지 말고 생성된 이미지를 직접 눈으로 확인하는 것도 중요해요.

숫자로는 좋아 보여도 이상한 아티팩트가 있을 수 있거든요." GAN의 세계는 여기서 끝이 아닙니다. StyleGAN, BigGAN, VQ-GAN 등 더 발전된 모델들이 계속 나오고 있습니다.

오늘 배운 기초를 바탕으로 더 깊이 탐구해 보시기 바랍니다.

실전 팁

💡 - pytorch-fid 라이브러리를 사용하면 FID를 쉽게 계산할 수 있습니다: pip install pytorch-fid

  • 학습 중에 주기적으로 FID를 계산해서 모델의 성능 변화를 추적하세요. 체크포인트 선택에 유용합니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#Python#GAN#DeepLearning#DCGAN#ImageGeneration#AI,DeepLearning,ComputerVision

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.