본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 9. · 12 Views
GAN으로 시작하는 이미지 생성 완벽 가이드
실제 같은 이미지를 만들어내는 GAN의 원리를 초급 개발자를 위해 쉽게 풀어냅니다. Generator와 Discriminator가 서로 경쟁하며 학습하는 과정을 실무 스토리와 함께 이해해봅시다.
목차
1. GAN의 기본 원리
김개발 씨는 회사에서 새로운 프로젝트를 맡게 되었습니다. "AI로 제품 이미지를 자동 생성하는 기능을 만들어보세요." 팀장님이 말씀하셨습니다.
김개발 씨는 GAN이라는 단어를 들어본 적은 있지만, 정확히 어떻게 동작하는지 몰라 막막했습니다.
GAN은 Generative Adversarial Networks의 약자로, 두 개의 신경망이 서로 경쟁하면서 학습하는 방식입니다. 마치 위조지폐범과 경찰이 서로 실력을 키워가는 것처럼, 한쪽은 가짜 이미지를 만들고 다른 쪽은 진짜와 가짜를 구분합니다.
이 과정을 반복하면서 점점 더 진짜 같은 이미지를 생성할 수 있게 됩니다.
다음 코드를 살펴봅시다.
import torch
import torch.nn as nn
# GAN의 기본 학습 루프 구조
def train_gan(generator, discriminator, real_images, epochs):
# 최적화 함수 설정
g_optimizer = torch.optim.Adam(generator.parameters(), lr=0.0002)
d_optimizer = torch.optim.Adam(discriminator.parameters(), lr=0.0002)
criterion = nn.BCELoss()
for epoch in range(epochs):
# Discriminator 학습: 진짜와 가짜를 구분
noise = torch.randn(batch_size, 100) # 랜덤 노이즈 생성
fake_images = generator(noise) # 가짜 이미지 생성
# Discriminator가 진짜와 가짜를 판별
d_loss = criterion(discriminator(real_images), torch.ones(batch_size)) + \
criterion(discriminator(fake_images.detach()), torch.zeros(batch_size))
# Generator 학습: Discriminator를 속이기
g_loss = criterion(discriminator(fake_images), torch.ones(batch_size))
김개발 씨는 선배 개발자인 박시니어 씨를 찾아갔습니다. "GAN이 도대체 뭔가요?
어떻게 AI가 이미지를 만들어낼 수 있는 건가요?" 박시니어 씨는 커피를 한 모금 마시고는 쉬운 비유로 설명을 시작했습니다. "GAN을 이해하려면 먼저 재미있는 이야기를 하나 들려드릴게요.
위조지폐범과 경찰의 싸움이라고 생각하면 됩니다." 위조지폐범은 처음에는 서툴게 돈을 만듭니다. 색깔도 이상하고 크기도 맞지 않죠.
경찰은 쉽게 가짜를 구분해냅니다. 하지만 위조지폐범은 포기하지 않습니다.
경찰의 피드백을 받아 점점 더 정교하게 돈을 만들어갑니다. 경찰도 가만히 있지 않습니다.
위조지폐범의 실력이 늘어나면, 경찰도 더 꼼꼼하게 진짜와 가짜를 구분하는 능력을 키웁니다. 이런 경쟁이 반복되면서 결국 위조지폐범은 거의 진짜와 구분할 수 없는 수준의 지폐를 만들어내게 됩니다.
GAN도 정확히 이런 방식으로 동작합니다. Generator는 위조지폐범 역할을 합니다.
랜덤한 노이즈를 입력받아서 이미지를 생성합니다. 처음에는 형편없는 이미지를 만들지만, 학습을 거듭하면서 점점 더 실제 같은 이미지를 만들어냅니다.
Discriminator는 경찰 역할을 합니다. 진짜 이미지와 Generator가 만든 가짜 이미지를 받아서 이것이 진짜인지 가짜인지 판별합니다.
처음에는 쉽게 구분하지만, Generator의 실력이 좋아질수록 판별이 어려워집니다. 이 두 신경망은 서로 경쟁하며 학습합니다.
이를 적대적 학습이라고 부릅니다. Adversarial이라는 단어가 바로 이 의미입니다.
김개발 씨가 물었습니다. "그런데 어떻게 서로 경쟁하게 만드나요?" 박시니어 씨가 설명을 이어갔습니다.
"각 신경망이 추구하는 목표가 정반대이기 때문입니다." Discriminator는 진짜 이미지에는 1점을 주고, 가짜 이미지에는 0점을 주려고 합니다. 반면 Generator는 Discriminator가 자신이 만든 가짜 이미지에 1점을 주도록 속이려고 합니다.
이 목표의 충돌이 학습을 이끌어냅니다. 위의 코드를 보면 이 과정이 구현되어 있습니다.
먼저 랜덤 노이즈를 생성합니다. 이것은 Generator의 입력이 됩니다.
Generator는 이 노이즈로부터 이미지를 생성합니다. 그 다음 Discriminator를 학습시킵니다.
진짜 이미지에는 1을 출력하도록, 가짜 이미지에는 0을 출력하도록 손실을 계산합니다. 마지막으로 Generator를 학습시킵니다.
Discriminator가 Generator가 만든 이미지를 진짜라고 판단하도록, 즉 1을 출력하도록 손실을 계산합니다. 이 과정이 수천, 수만 번 반복되면서 Generator는 점점 더 진짜 같은 이미지를 만들어내게 됩니다.
실제 현업에서는 어떻게 활용할까요? 패션 업계에서는 GAN을 사용해 새로운 의상 디자인을 생성합니다.
게임 회사에서는 캐릭터나 배경을 자동으로 만들어냅니다. 광고 회사에서는 모델의 얼굴을 합성하거나 제품 이미지를 생성하는 데 활용합니다.
김개발 씨는 이제 GAN의 기본 원리를 이해했습니다. "와, 정말 신기하네요.
그런데 실제로 구현하려면 어떻게 해야 하나요?" 박시니어 씨가 웃으며 말했습니다. "이제 본격적으로 Generator와 Discriminator의 구조를 살펴볼 차례입니다."
실전 팁
💡 - Generator와 Discriminator의 학습 균형이 중요합니다. 한쪽이 너무 강해지면 학습이 불안정해집니다.
- 초기에는 낮은 해상도로 시작해서 점진적으로 높이는 것이 효과적입니다.
2. Generator와 Discriminator
박시니어 씨는 화이트보드 앞에 섰습니다. "자, 이제 각 신경망의 역할을 자세히 알아볼까요?" 김개발 씨는 노트북을 열고 메모할 준비를 했습니다.
Generator는 랜덤 노이즈를 입력받아 이미지를 생성하는 신경망입니다. Discriminator는 이미지를 입력받아 진짜인지 가짜인지 확률값을 출력하는 신경망입니다.
두 신경망은 일반적으로 컨볼루션 레이어로 구성되며, Generator는 업샘플링을, Discriminator는 다운샘플링을 수행합니다.
다음 코드를 살펴봅시다.
import torch.nn as nn
# Generator 네트워크 구조
class Generator(nn.Module):
def __init__(self, latent_dim=100):
super(Generator, self).__init__()
# 노이즈를 이미지로 변환하는 네트워크
self.model = nn.Sequential(
nn.Linear(latent_dim, 256), # 첫 번째 확장
nn.ReLU(),
nn.Linear(256, 512), # 더 확장
nn.ReLU(),
nn.Linear(512, 1024), # 계속 확장
nn.ReLU(),
nn.Linear(1024, 28*28), # 최종 이미지 크기
nn.Tanh() # -1에서 1 사이 값으로 정규화
)
def forward(self, z):
# z는 랜덤 노이즈 벡터
img = self.model(z)
return img.view(img.size(0), 1, 28, 28)
# Discriminator 네트워크 구조
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
# 이미지를 진짜/가짜로 판별하는 네트워크
self.model = nn.Sequential(
nn.Flatten(), # 이미지를 1차원으로
nn.Linear(28*28, 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):
# 이미지가 진짜일 확률 반환
validity = self.model(img)
return validity
박시니어 씨가 화이트보드에 두 개의 큰 박스를 그렸습니다. "왼쪽이 Generator, 오른쪽이 Discriminator입니다." 먼저 Generator부터 살펴봅시다.
Generator의 입력은 잠재 벡터(latent vector)라고 부르는 랜덤 노이즈입니다. 보통 100차원 정도의 벡터를 사용합니다.
이 노이즈는 마치 DNA 정보와 같습니다. 같은 노이즈를 넣으면 항상 같은 이미지가 나오고, 다른 노이즈를 넣으면 다른 이미지가 나옵니다.
Generator는 이 작은 벡터를 점점 크게 확장시킵니다. 100차원에서 256차원, 512차원, 1024차원으로 늘려가면서 이미지의 정보를 만들어냅니다.
마지막에는 28x28 픽셀의 이미지 크기로 변환됩니다. 여기서 중요한 것은 활성화 함수의 선택입니다.
Generator의 중간 레이어에서는 ReLU를 사용하고, 마지막 레이어에서는 Tanh를 사용합니다. Tanh는 -1에서 1 사이의 값을 출력하는데, 이미지 픽셀값을 이 범위로 정규화하는 것이 학습에 유리하기 때문입니다.
김개발 씨가 질문했습니다. "그럼 Discriminator는 어떻게 다른가요?" 박시니어 씨가 오른쪽 박스를 가리키며 설명했습니다.
Discriminator는 Generator와 정반대의 일을 합니다. 이미지를 입력받아서 점점 작은 차원으로 압축시킵니다.
28x28 픽셀의 이미지를 먼저 1차원 벡터로 펼칩니다. 그 다음 512차원, 256차원으로 줄여가면서 중요한 특징만 추출합니다.
최종적으로는 단 하나의 값을 출력합니다. 이 하나의 값이 바로 "이 이미지가 진짜일 확률"입니다.
Discriminator의 중간 레이어에서는 LeakyReLU를 사용합니다. 일반 ReLU와 다르게 음수 값에도 작은 기울기를 줍니다.
이것이 Discriminator의 학습을 더 안정적으로 만들어준다고 알려져 있습니다. 마지막 레이어에서는 Sigmoid 함수를 사용합니다.
Sigmoid는 어떤 값이든 0에서 1 사이로 압축시킵니다. 1에 가까우면 진짜, 0에 가까우면 가짜라고 판단하는 것입니다.
실제로 코드를 보면 이 구조가 명확하게 드러납니다. Generator의 forward 함수는 랜덤 노이즈 z를 받아서 이미지로 변환합니다.
마지막에 view 함수로 1차원 벡터를 28x28 이미지 형태로 reshape합니다. Discriminator의 forward 함수는 이미지를 받아서 먼저 Flatten으로 1차원으로 펼칩니다.
그 다음 여러 레이어를 거쳐 최종적으로 진짜일 확률값을 반환합니다. 김개발 씨가 코드를 유심히 살펴보더니 물었습니다.
"이 구조는 고정된 건가요? 항상 이렇게 해야 하나요?" 박시니어 씨가 고개를 저었습니다.
"아니요, 이것은 가장 기본적인 구조입니다. 실제로는 더 복잡한 구조를 많이 사용하죠." 예를 들어 고해상도 이미지를 생성하려면 컨볼루션 레이어를 사용해야 합니다.
이를 DCGAN이라고 부르는데, 이것은 나중에 자세히 살펴볼 예정입니다. 또한 Generator와 Discriminator의 용량을 비슷하게 맞춰주는 것이 중요합니다.
한쪽이 너무 강하면 학습이 제대로 이루어지지 않습니다. 마치 프로 복서와 초보 복서가 대결하면 경기가 되지 않는 것과 같습니다.
김개발 씨는 이제 두 신경망의 구조를 이해했습니다. "그럼 이 둘이 어떻게 함께 학습되는 건가요?"
실전 팁
💡 - Generator는 Tanh 활성화로 -1~1 범위 출력, 실제 이미지도 같은 범위로 정규화해야 합니다.
- Discriminator의 LeakyReLU는 기울기 소실 문제를 완화합니다.
- 두 네트워크의 파라미터 수를 비슷하게 맞추면 학습이 안정적입니다.
3. 적대적 학습 과정
"이제 가장 중요한 부분입니다." 박시니어 씨가 말했습니다. 김개발 씨는 두 신경망이 어떻게 서로 경쟁하면서 학습하는지 궁금했습니다.
적대적 학습은 Generator와 Discriminator를 번갈아가며 학습시키는 과정입니다. 먼저 Discriminator를 진짜와 가짜를 잘 구분하도록 학습시키고, 그 다음 Generator를 Discriminator를 속이도록 학습시킵니다.
이 과정을 반복하면서 두 신경망이 함께 발전합니다.
다음 코드를 살펴봅시다.
import torch
import torch.nn as nn
def train_step(generator, discriminator, real_images, latent_dim, device):
batch_size = real_images.size(0)
criterion = nn.BCELoss()
# 진짜/가짜 레이블 생성
real_labels = torch.ones(batch_size, 1).to(device)
fake_labels = torch.zeros(batch_size, 1).to(device)
# ========== Discriminator 학습 ==========
discriminator.train()
generator.eval()
# 진짜 이미지에 대한 손실
real_outputs = discriminator(real_images)
d_loss_real = criterion(real_outputs, real_labels)
# 가짜 이미지 생성 및 손실 계산
z = torch.randn(batch_size, latent_dim).to(device)
fake_images = generator(z)
fake_outputs = discriminator(fake_images.detach()) # detach로 Generator 그래디언트 차단
d_loss_fake = criterion(fake_outputs, fake_labels)
# Discriminator 전체 손실
d_loss = d_loss_real + d_loss_fake
# ========== Generator 학습 ==========
generator.train()
discriminator.eval()
# 가짜 이미지로 Discriminator를 속이기
z = torch.randn(batch_size, latent_dim).to(device)
fake_images = generator(z)
outputs = discriminator(fake_images)
g_loss = criterion(outputs, real_labels) # 가짜를 진짜로 인식시키기
return d_loss, g_loss
박시니어 씨가 화이트보드에 학습 과정을 그리기 시작했습니다. "적대적 학습은 마치 탁구 경기와 같습니다." 한 선수가 공을 치면, 다른 선수가 받아칩니다.
그러면 첫 번째 선수가 다시 받아칩니다. 이렇게 주고받으면서 둘 다 실력이 늘어갑니다.
GAN도 똑같습니다. 먼저 Discriminator 학습 단계를 살펴봅시다.
Discriminator는 두 가지 데이터로 학습합니다. 진짜 이미지와 가짜 이미지입니다.
진짜 이미지를 보여주고 "이것은 진짜입니다"라고 가르칩니다. 가짜 이미지를 보여주고 "이것은 가짜입니다"라고 가르칩니다.
코드를 보면 real_labels는 1로, fake_labels는 0으로 설정되어 있습니다. Discriminator가 진짜 이미지에는 1을 출력하고, 가짜 이미지에는 0을 출력하도록 손실을 계산합니다.
여기서 중요한 것은 detach() 함수입니다. 가짜 이미지를 Discriminator에 넣을 때 detach()를 사용합니다.
이것은 "여기서는 Generator를 학습시키지 마세요"라는 의미입니다. Discriminator를 학습시킬 때는 오직 Discriminator의 가중치만 업데이트해야 하기 때문입니다.
김개발 씨가 끄덕이며 물었습니다. "그럼 Generator는 언제 학습하나요?" 박시니어 씨가 설명을 이어갔습니다.
"바로 그 다음입니다." Generator 학습 단계는 더 흥미롭습니다. Generator는 새로운 가짜 이미지를 생성합니다.
그리고 이것을 Discriminator에 넣습니다. 하지만 이번에는 목표가 다릅니다.
Discriminator가 이 가짜 이미지를 진짜라고 판단하도록 만들고 싶은 것입니다. 코드를 보면 가짜 이미지를 만들고, Discriminator에 넣고, 손실을 계산할 때 real_labels를 사용합니다.
"이 가짜 이미지가 진짜로 보이도록 만들어라"는 의미입니다. 이것이 바로 적대적의 핵심입니다.
Discriminator는 "가짜를 가짜라고 판단하라"고 학습받습니다. Generator는 "가짜를 진짜처럼 만들어라"고 학습받습니다.
둘의 목표가 정반대입니다. 이 충돌이 학습을 이끌어냅니다.
실제 학습 과정을 시각화해보면 이렇습니다. 초기에는 Generator가 만든 이미지가 형편없습니다.
Discriminator는 쉽게 가짜를 구분합니다. Discriminator의 손실은 낮고, Generator의 손실은 높습니다.
점점 Generator가 발전하면서 더 그럴듯한 이미지를 만듭니다. Discriminator는 구분이 어려워지기 시작합니다.
Discriminator의 손실이 올라가고, Generator의 손실이 내려갑니다. Discriminator도 가만히 있지 않습니다.
더 정교하게 진짜와 가짜를 구분하는 법을 배웁니다. 다시 Discriminator의 손실이 내려갑니다.
이런 과정이 반복되면서 둘 다 점점 더 강해집니다. 이상적으로는 Discriminator가 진짜와 가짜를 50% 확률로 판단할 때, 즉 전혀 구분하지 못할 때 학습이 완료됩니다.
김개발 씨가 걱정스러운 표정을 지었습니다. "그런데 한쪽이 너무 강해지면 어떻게 되나요?" 박시니어 씨가 고개를 끄덕였습니다.
"좋은 질문입니다. 그것이 GAN의 가장 큰 어려움 중 하나입니다." 만약 Discriminator가 너무 강해지면 Generator가 학습할 기회를 잃습니다.
Generator가 무엇을 만들어도 Discriminator가 바로 가짜라고 판단해버리기 때문입니다. 반대로 Generator가 너무 강해지면 Discriminator가 항상 속게 되고, 제대로 된 피드백을 줄 수 없습니다.
따라서 두 신경망의 균형을 맞추는 것이 매우 중요합니다. 학습률을 조정하거나, 네트워크 크기를 조절하거나, 학습 횟수를 다르게 가져가는 등의 기법을 사용합니다.
실제 프로젝트에서는 이 균형을 찾는 것이 가장 어려운 부분입니다. 수많은 실험과 조정이 필요합니다.
실전 팁
💡 - Discriminator를 여러 번 학습시킨 후 Generator를 한 번 학습시키면 안정적일 수 있습니다.
- 손실값을 모니터링하여 한쪽이 너무 낮거나 높으면 학습률을 조정하세요.
- detach()를 빼먹으면 그래디언트가 잘못 전파되어 학습이 실패할 수 있습니다.
4. DCGAN 구조
며칠 후, 김개발 씨는 기본 GAN으로 MNIST 숫자를 생성하는 데 성공했습니다. 하지만 팀장님은 더 고해상도의 컬러 이미지를 원했습니다.
박시니어 씨가 말했습니다. "이제 DCGAN을 배울 때가 됐네요."
DCGAN은 Deep Convolutional GAN의 약자로, 컨볼루션 레이어를 사용하는 GAN입니다. 기본 GAN의 Fully Connected 레이어 대신 Conv2d와 ConvTranspose2d를 사용합니다.
이를 통해 더 고해상도의 이미지를 생성할 수 있으며, 배치 정규화와 특정 활성화 함수를 사용해 학습을 안정화합니다.
다음 코드를 살펴봅시다.
import torch.nn as nn
# DCGAN Generator 구조
class DCGenerator(nn.Module):
def __init__(self, latent_dim=100, img_channels=3):
super(DCGenerator, self).__init__()
# 컨볼루션 레이어로 이미지 생성
self.model = nn.Sequential(
# 입력: (latent_dim) x 1 x 1
nn.ConvTranspose2d(latent_dim, 512, 4, 1, 0, bias=False),
nn.BatchNorm2d(512),
nn.ReLU(True),
# 크기: 512 x 4 x 4
nn.ConvTranspose2d(512, 256, 4, 2, 1, bias=False),
nn.BatchNorm2d(256),
nn.ReLU(True),
# 크기: 256 x 8 x 8
nn.ConvTranspose2d(256, 128, 4, 2, 1, bias=False),
nn.BatchNorm2d(128),
nn.ReLU(True),
# 크기: 128 x 16 x 16
nn.ConvTranspose2d(128, 64, 4, 2, 1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(True),
# 크기: 64 x 32 x 32
nn.ConvTranspose2d(64, img_channels, 4, 2, 1, bias=False),
nn.Tanh()
# 출력: img_channels x 64 x 64
)
def forward(self, z):
z = z.view(z.size(0), z.size(1), 1, 1)
return self.model(z)
# DCGAN Discriminator 구조
class DCDiscriminator(nn.Module):
def __init__(self, img_channels=3):
super(DCDiscriminator, self).__init__()
self.model = nn.Sequential(
# 입력: img_channels x 64 x 64
nn.Conv2d(img_channels, 64, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# 크기: 64 x 32 x 32
nn.Conv2d(64, 128, 4, 2, 1, bias=False),
nn.BatchNorm2d(128),
nn.LeakyReLU(0.2, inplace=True),
# 크기: 128 x 16 x 16
nn.Conv2d(128, 256, 4, 2, 1, bias=False),
nn.BatchNorm2d(256),
nn.LeakyReLU(0.2, inplace=True),
# 크기: 256 x 8 x 8
nn.Conv2d(256, 512, 4, 2, 1, bias=False),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.2, inplace=True),
# 크기: 512 x 4 x 4
nn.Conv2d(512, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
# 출력: 1 x 1 x 1
)
def forward(self, img):
validity = self.model(img)
return validity.view(-1, 1)
박시니어 씨가 새로운 코드 파일을 열었습니다. "기본 GAN은 Fully Connected 레이어만 사용했습니다.
하지만 이미지에는 공간적 구조가 있죠." 예를 들어 고양이 사진을 생각해봅시다. 귀는 머리 위쪽에, 눈은 얼굴 중앙에, 코는 그 아래에 있습니다.
이런 공간적 관계를 Fully Connected 레이어는 제대로 학습하기 어렵습니다. 컨볼루션 레이어는 이미지의 공간적 구조를 유지하면서 학습합니다.
DCGAN은 2015년에 발표되었는데, GAN의 역사에서 큰 전환점이었습니다. 처음으로 안정적으로 고해상도 이미지를 생성할 수 있게 되었기 때문입니다.
김개발 씨가 코드를 살펴보며 물었습니다. "ConvTranspose2d는 뭔가요?" 박시니어 씨가 설명했습니다.
"이것은 역컨볼루션 또는 전치 컨볼루션이라고 부릅니다." 일반 컨볼루션은 이미지 크기를 줄입니다. 64x64 이미지를 32x32로, 32x32를 16x16으로 줄여갑니다.
반면 ConvTranspose2d는 반대로 크기를 늘립니다. 4x4를 8x8로, 8x8을 16x16으로 확대합니다.
Generator는 작은 노이즈 벡터에서 시작해서 점점 큰 이미지로 만들어야 하므로 ConvTranspose2d를 사용합니다. 코드를 단계별로 살펴봅시다.
Generator는 100차원 벡터를 입력받아 먼저 1x1 크기로 reshape합니다. 첫 번째 ConvTranspose2d가 이것을 4x4 크기의 512채널 특징맵으로 만듭니다.
그 다음 8x8, 16x16, 32x32로 계속 확대되고, 마지막에 64x64 크기의 3채널(RGB) 이미지가 됩니다. 각 레이어 뒤에는 BatchNorm2d가 붙어있습니다.
배치 정규화는 DCGAN의 핵심 기법 중 하나입니다. 각 레이어의 출력을 정규화하여 학습을 안정화시킵니다.
GAN은 원래 학습이 불안정한데, 배치 정규화가 이를 크게 개선합니다. 활성화 함수도 주의깊게 선택되었습니다.
Generator는 ReLU를, Discriminator는 LeakyReLU를 사용합니다. 마지막 레이어에서 Generator는 Tanh, Discriminator는 Sigmoid를 사용합니다.
Discriminator는 Generator의 거울상입니다. 64x64 이미지를 입력받아 일반 Conv2d로 크기를 점점 줄여갑니다.
32x32, 16x16, 8x8, 4x4로 줄어들고, 마지막에는 1x1 크기의 단일 값이 됩니다. 이 값이 진짜일 확률입니다.
김개발 씨가 실행해보더니 놀랐습니다. "와, 기본 GAN보다 훨씬 선명한 이미지가 나오네요!" 박시니어 씨가 웃으며 말했습니다.
"DCGAN의 설계 원칙은 매우 중요합니다. 논문에서 제시한 가이드라인을 따르면 안정적으로 학습됩니다." 그 원칙은 다음과 같습니다.
Pooling 레이어 대신 stride를 사용한 컨볼루션으로 다운샘플링합니다. Batch Normalization을 Generator와 Discriminator 모두에 사용합니다.
Fully Connected 레이어를 제거합니다. Generator는 ReLU, Discriminator는 LeakyReLU를 사용합니다.
실제 산업에서는 DCGAN이 많은 응용 프로그램의 기초가 됩니다. 얼굴 생성, 만화 캐릭터 생성, 인테리어 디자인, 의상 디자인 등에서 DCGAN 구조를 변형하여 사용합니다.
최근의 고급 GAN들(StyleGAN, BigGAN 등)도 DCGAN의 기본 구조를 확장한 것입니다. 김개발 씨는 이제 실전에서 사용할 수 있는 GAN을 이해하게 되었습니다.
실전 팁
💡 - stride와 padding을 잘 설정하면 정확한 크기의 이미지를 생성할 수 있습니다.
- BatchNorm을 Generator의 출력층과 Discriminator의 입력층에는 사용하지 마세요.
- 학습률은 0.0002, beta1은 0.5로 설정하는 것이 일반적입니다.
5. Mode Collapse 문제
김개발 씨는 DCGAN으로 얼굴 이미지를 생성하는 프로젝트를 진행했습니다. 처음에는 다양한 얼굴이 나왔는데, 학습이 진행될수록 비슷한 얼굴만 반복해서 생성되었습니다.
"이게 뭐죠?" 김개발 씨가 당황하며 박시니어 씨를 불렀습니다.
Mode Collapse는 Generator가 다양한 샘플 대신 몇 가지 유사한 샘플만 반복해서 생성하는 현상입니다. Generator가 Discriminator를 속이는 쉬운 방법을 찾아 그것만 반복하는 것입니다.
이는 GAN의 가장 큰 문제 중 하나이며, 학습 불안정성의 주요 원인입니다.
다음 코드를 살펴봅시다.
import torch
import torch.nn as nn
# Minibatch Discrimination으로 Mode Collapse 완화
class MinibatchDiscrimination(nn.Module):
def __init__(self, in_features, out_features, kernel_dims):
super(MinibatchDiscrimination, self).__init__()
self.in_features = in_features
self.out_features = out_features
self.kernel_dims = kernel_dims
# 미니배치 통계를 위한 텐서
self.T = nn.Parameter(torch.randn(in_features, out_features, kernel_dims))
def forward(self, x):
# 배치 내 샘플들 간의 유사도 계산
matrices = x.mm(self.T.view(self.in_features, -1))
matrices = matrices.view(-1, self.out_features, self.kernel_dims)
# 각 샘플 쌍의 L1 거리 계산
M = matrices.unsqueeze(0)
M_T = matrices.unsqueeze(1)
norm = torch.abs(M - M_T).sum(3)
expnorm = torch.exp(-norm)
# 자기 자신 제외하고 합산
o_b = (expnorm.sum(0) - 1)
return torch.cat([x, o_b], 1)
# Feature Matching Loss
def feature_matching_loss(discriminator, real_images, fake_images):
# 중간 레이어의 특징을 매칭
real_features = discriminator.extract_features(real_images)
fake_features = discriminator.extract_features(fake_images)
# 특징의 평균 차이를 최소화
loss = torch.mean(torch.abs(real_features.mean(0) - fake_features.mean(0)))
return loss
# Unrolled GAN으로 Mode Collapse 방지
def unrolled_discriminator_step(generator, discriminator, real_images,
latent_dim, unroll_steps=5):
# Discriminator를 여러 스텝 앞서 학습시켜 예측
backup = [p.clone() for p in discriminator.parameters()]
for _ in range(unroll_steps):
# Discriminator 학습 시뮬레이션
z = torch.randn(real_images.size(0), latent_dim)
fake_images = generator(z)
d_loss = discriminator_loss(discriminator, real_images, fake_images)
# 가상으로 파라미터 업데이트
grads = torch.autograd.grad(d_loss, discriminator.parameters(),
create_graph=True)
for param, grad in zip(discriminator.parameters(), grads):
param.data = param.data - 0.01 * grad
# Generator 손실 계산 (미래의 Discriminator 기준)
z = torch.randn(real_images.size(0), latent_dim)
fake_images = generator(z)
g_loss = generator_loss(discriminator, fake_images)
# Discriminator 파라미터 복원
for param, backup_param in zip(discriminator.parameters(), backup):
param.data = backup_param
return g_loss
박시니어 씨가 모니터를 살펴보고는 한숨을 쉬었습니다. "아, Mode Collapse가 발생했네요.
GAN의 고질적인 문제입니다." Mode Collapse란 무엇일까요? 쉽게 비유하자면, 학생이 시험에서 한 가지 답만 반복해서 쓰는 것과 같습니다.
예를 들어 수학 문제에서 모든 답을 "5"라고 쓰면 어떨까요? 몇 문제는 맞을 수 있습니다.
하지만 다양한 문제를 풀 수 있는 진짜 실력은 아닙니다. Generator도 마찬가지입니다.
Discriminator를 속일 수 있는 특정 이미지를 발견하면, 계속 그것만 생성합니다. 다양한 이미지를 만들 필요가 없어지는 것입니다.
목표는 Discriminator를 속이는 것뿐이니까요. 김개발 씨가 물었습니다.
"왜 이런 일이 발생하나요?" 박시니어 씨가 설명했습니다. "GAN의 학습 목표 자체에 문제가 있습니다." Generator는 다양한 이미지를 만들라는 명령을 받지 않습니다.
단지 Discriminator를 속이라는 명령만 받습니다. 따라서 쉬운 지름길을 찾으면 그것만 사용하게 됩니다.
실제로 Mode Collapse는 여러 형태로 나타납니다. Complete Collapse는 Generator가 단 하나의 샘플만 생성하는 극단적인 경우입니다.
어떤 노이즈를 넣어도 같은 이미지가 나옵니다. Partial Collapse는 몇 가지 유사한 샘플을 번갈아가며 생성하는 경우입니다.
김개발 씨가 겪은 문제가 바로 이것입니다. 전체 데이터의 다양성을 포착하지 못하고 일부 패턴만 학습합니다.
그렇다면 어떻게 해결할 수 있을까요? 여러 기법이 제안되었습니다.
Minibatch Discrimination은 배치 내 샘플들의 다양성을 명시적으로 측정합니다. Generator가 비슷한 샘플을 여러 개 생성하면 Discriminator가 이를 감지하고 가짜로 판단합니다.
따라서 Generator는 다양한 샘플을 만들도록 유도됩니다. 코드를 보면 배치 내 샘플들 간의 거리를 계산합니다.
샘플들이 서로 비슷하면 거리가 가깝고, 다양하면 거리가 멉니다. 이 정보를 Discriminator의 판단에 포함시킵니다.
Feature Matching은 학습 목표를 바꾸는 방법입니다. Generator가 Discriminator의 최종 출력을 속이려 하는 대신, 중간 레이어의 특징을 매칭하도록 합니다.
진짜 이미지의 특징과 가짜 이미지의 특징이 비슷해지도록 학습합니다. 이렇게 하면 특정 샘플에 과적합되는 것을 방지할 수 있습니다.
Unrolled GAN은 더 영리한 방법입니다. Generator를 학습할 때, "만약 Discriminator가 앞으로 K번 더 학습한다면 어떻게 될까?"를 미리 계산합니다.
미래의 Discriminator를 기준으로 Generator를 학습시키는 것입니다. 이렇게 하면 Discriminator가 적응할 수 있는 샘플을 생성하게 되어 Mode Collapse를 피할 수 있습니다.
코드를 보면 Discriminator의 파라미터를 백업하고, 가상으로 여러 스텝 학습시킵니다. 그 다음 미래의 Discriminator 기준으로 Generator 손실을 계산하고, 다시 Discriminator를 원래대로 복원합니다.
실제 프로젝트에서는 어떻게 대응할까요? 먼저 Mode Collapse를 조기에 감지하는 것이 중요합니다.
생성된 샘플의 다양성을 주기적으로 체크합니다. Inception Score나 FID(Frechet Inception Distance) 같은 지표를 사용할 수 있습니다.
Mode Collapse가 감지되면 학습률을 낮추거나, 다른 초기화로 재시작하거나, 위의 기법들을 적용합니다. 김개발 씨가 고개를 끄덕였습니다.
"그럼 이런 문제를 완전히 해결한 방법은 없나요?" 박시니어 씨가 답했습니다. "Wasserstein GAN이나 StyleGAN 같은 최신 방법들이 이 문제를 많이 개선했습니다.
하지만 여전히 완벽한 해결책은 없어요. GAN 연구의 중요한 주제입니다."
실전 팁
💡 - 생성된 샘플의 다양성을 시각적으로 모니터링하세요. 비슷한 이미지가 반복되면 조기 신호입니다.
- 여러 체크포인트를 저장하고, Mode Collapse 이전 상태로 롤백할 수 있게 준비하세요.
- 학습 초기에 낮은 학습률로 시작하면 Mode Collapse를 지연시킬 수 있습니다.
6. GAN 학습 팁
김개발 씨는 GAN의 기본 원리를 모두 배웠습니다. 하지만 실제로 학습을 시키려니 수많은 시행착오가 있었습니다.
박시니어 씨가 자신의 노하우를 공유하기로 했습니다.
GAN 학습은 매우 까다롭고 하이퍼파라미터에 민감합니다. 학습률, 배치 크기, 네트워크 구조, 정규화 방법 등 모든 것이 결과에 영향을 미칩니다.
안정적인 학습을 위해서는 검증된 설정값을 사용하고, 손실과 샘플을 주기적으로 모니터링해야 합니다.
다음 코드를 살펴봅시다.
import torch
import torch.nn as nn
from torchvision.utils import save_image
import matplotlib.pyplot as plt
# GAN 학습 베스트 프랙티스
def train_gan_stable(generator, discriminator, dataloader, num_epochs):
# Adam 옵티마이저 설정 (beta1=0.5가 중요)
lr = 0.0002
g_optimizer = torch.optim.Adam(generator.parameters(), lr=lr, betas=(0.5, 0.999))
d_optimizer = torch.optim.Adam(discriminator.parameters(), lr=lr, betas=(0.5, 0.999))
# 레이블 스무딩으로 안정화
real_label = 0.9 # 1.0 대신 0.9 사용
fake_label = 0.0
criterion = nn.BCELoss()
# 손실 기록
g_losses = []
d_losses = []
for epoch in range(num_epochs):
for i, (real_images, _) in enumerate(dataloader):
batch_size = real_images.size(0)
# ========== Discriminator 학습 ==========
discriminator.zero_grad()
# 진짜 이미지 학습
real_labels = torch.full((batch_size, 1), real_label)
real_output = discriminator(real_images)
d_loss_real = criterion(real_output, real_labels)
# 가짜 이미지 학습
noise = torch.randn(batch_size, 100)
fake_images = generator(noise)
fake_labels = torch.full((batch_size, 1), fake_label)
fake_output = discriminator(fake_images.detach())
d_loss_fake = criterion(fake_output, fake_labels)
# Discriminator 업데이트
d_loss = d_loss_real + d_loss_fake
d_loss.backward()
d_optimizer.step()
# ========== Generator 학습 ==========
generator.zero_grad()
# Discriminator를 속이기
noise = torch.randn(batch_size, 100)
fake_images = generator(noise)
fake_output = discriminator(fake_images)
g_loss = criterion(fake_output, torch.ones(batch_size, 1))
# Generator 업데이트
g_loss.backward()
g_optimizer.step()
# 손실 기록
g_losses.append(g_loss.item())
d_losses.append(d_loss.item())
# 주기적으로 샘플 저장
if i % 100 == 0:
print(f'Epoch [{epoch}/{num_epochs}], Step [{i}/{len(dataloader)}], '
f'D Loss: {d_loss.item():.4f}, G Loss: {g_loss.item():.4f}')
save_image(fake_images[:25], f'samples_epoch_{epoch}_step_{i}.png', nrow=5)
# 에폭마다 체크포인트 저장
torch.save({
'epoch': epoch,
'generator': generator.state_dict(),
'discriminator': discriminator.state_dict(),
'g_optimizer': g_optimizer.state_dict(),
'd_optimizer': d_optimizer.state_dict(),
}, f'checkpoint_epoch_{epoch}.pth')
# 손실 그래프 그리기
plt.figure(figsize=(10, 5))
plt.plot(g_losses, label='Generator Loss')
plt.plot(d_losses, label='Discriminator Loss')
plt.xlabel('Iterations')
plt.ylabel('Loss')
plt.legend()
plt.savefig('training_losses.png')
박시니어 씨가 자신의 노트북을 열며 말했습니다. "GAN 학습은 예술에 가깝습니다.
이론도 중요하지만 실전 경험이 더 중요하죠." 첫 번째로 중요한 것은 학습률 설정입니다. 대부분의 GAN 논문에서 0.0002를 사용합니다.
이것은 많은 실험을 통해 검증된 값입니다. 너무 높으면 학습이 발산하고, 너무 낮으면 학습이 느리거나 Mode Collapse에 빠집니다.
Adam 옵티마이저의 beta1 값도 중요합니다. 일반적으로 0.9를 사용하지만, GAN에서는 0.5를 사용하는 것이 좋습니다.
이것은 모멘텀을 줄여서 학습을 안정화시킵니다. 두 번째는 레이블 스무딩입니다.
코드를 보면 real_label을 1.0이 아닌 0.9로 설정했습니다. 이것은 Discriminator가 너무 자신만만해지는 것을 방지합니다.
Discriminator가 진짜 이미지에 항상 1을 출력하도록 학습되면, Generator가 학습할 기회를 잃습니다. 0.9로 설정하면 약간의 불확실성을 남겨둡니다.
세 번째는 배치 크기입니다. 너무 작은 배치는 학습을 불안정하게 만듭니다.
최소 32, 가능하면 64 이상을 사용하세요. 배치 정규화가 제대로 작동하려면 충분한 배치 크기가 필요합니다.
김개발 씨가 질문했습니다. "손실값을 보면 학습이 잘 되고 있는지 어떻게 알 수 있나요?" 박시니어 씨가 웃으며 답했습니다.
"그게 GAN의 까다로운 점입니다. 손실값만으로는 판단하기 어렵습니다." 일반적인 신경망은 손실이 낮아질수록 좋습니다.
하지만 GAN은 다릅니다. Generator와 Discriminator가 균형을 이루어야 합니다.
이상적으로는 두 손실이 비슷한 수준에서 왔다갔다 해야 합니다. 만약 Discriminator 손실이 0에 가깝고 Generator 손실이 매우 높다면, Discriminator가 너무 강한 것입니다.
반대로 Generator 손실이 낮고 Discriminator 손실이 높다면, Generator가 Discriminator를 완전히 속이고 있는 것인데, 이것도 Mode Collapse의 신호일 수 있습니다. 가장 확실한 방법은 생성된 샘플을 직접 보는 것입니다. 코드를 보면 100번째 스텝마다 샘플을 이미지로 저장합니다.
이 이미지들을 육안으로 확인하면서 품질이 개선되는지 체크해야 합니다. 초기에는 노이즈 같지만, 점점 형태가 잡히고, 디테일이 살아나야 합니다.
네 번째는 체크포인트 저장입니다. GAN 학습은 불안정하므로 주기적으로 모델을 저장해야 합니다.
나중에 Mode Collapse가 발생하면 이전 체크포인트로 돌아갈 수 있습니다. 에폭마다 저장하고, 특히 좋은 샘플이 나왔을 때는 별도로 저장해두세요.
다섯 번째는 데이터 전처리입니다. 이미지를 -1에서 1 사이로 정규화하세요.
Generator가 Tanh를 사용한다면 출력 범위가 -1에서 1이므로, 실제 이미지도 같은 범위여야 합니다. 또한 데이터를 충분히 섞고, 다양한 augmentation을 적용하면 도움이 됩니다.
여섯 번째는 초기화입니다. 가중치 초기화도 중요합니다.
DCGAN 논문에서는 평균 0, 표준편차 0.02인 정규분포로 초기화할 것을 권장합니다. PyTorch의 기본 초기화도 괜찮지만, 커스텀 초기화를 사용하면 더 안정적일 수 있습니다.
실제 프로젝트에서 박시니어 씨가 겪은 경험을 공유했습니다. "한번은 GAN을 일주일 동안 학습시켰는데 결과가 좋지 않았습니다.
알고 보니 데이터 정규화를 잘못했더라고요. 0-1 범위로 정규화했는데, Generator는 -1에서 1 범위로 출력하고 있었습니다.
이런 작은 실수가 전체 학습을 망칠 수 있습니다." 일곱 번째는 인내심입니다. GAN은 학습이 느립니다.
좋은 결과를 얻으려면 수천 에폭이 필요할 수도 있습니다. 중간에 포기하지 말고, 샘플을 계속 확인하면서 개선되는지 체크하세요.
어느 순간 갑자기 품질이 확 좋아지는 경우도 많습니다. 김개발 씨는 이제 실전에서 GAN을 학습시킬 준비가 되었습니다.
"감사합니다, 선배님. 이제 자신있게 프로젝트를 진행할 수 있을 것 같아요!" 박시니어 씨가 어깨를 두드리며 말했습니다.
"GAN은 처음에는 어렵지만, 한번 감을 잡으면 정말 재미있습니다. 상상하는 이미지를 AI가 만들어내는 것은 언제 봐도 신기하죠."
실전 팁
💡 - 학습 초기에는 낮은 해상도로 시작해서 점진적으로 높이면 안정적입니다.
- Tensorboard나 Weights & Biases 같은 도구로 손실과 샘플을 실시간 모니터링하세요.
- GPU 메모리가 부족하면 Gradient Accumulation을 사용해 배치 크기를 늘린 효과를 낼 수 있습니다.
- 여러 시드로 학습해보고 가장 안정적인 설정을 찾으세요.
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.