우리가 맞춰야 하는 것은 라벨이 아니다
이미지의 분포 값
nn.Tanh()를 사용하는 이유: 출력값을 [-1, 1] 범위로 제한Generator (생성자)
Discriminator (판별자)
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
self.model = nn.Sequential(
nn.Linear(3, 5), # 3차원 노이즈 → 5차원
nn.LeakyReLU(0.2),
nn.Linear(5, 2*2), # 5차원 → 4차원 (2×2 이미지)
nn.Tanh() # 출력값을 [-1, 1] 범위로
)
def forward(self, z):
return self.model(z).view(-1, 1, 2, 2) # (배치, 채널, 높이, 너비)
예시: 3차원 노이즈 → 2×2 이미지
입력 노이즈:
z = [0.5, -0.3, 0.8]
첫 번째 Linear 레이어 (3 → 5)
가중치와 편향:
W₁ = [[0.0762, -0.3221, -0.2734],
[0.2199, -0.2722, -0.2088],
[-0.3877, -0.3328, -0.3437],
[0.2889, -0.5252, 0.5153],
[0.2175, 0.1663, -0.0094]]
b₁ = [-0.3234, 0.2513, 0.3487, -0.5297, 0.2759]
계산:
출력₁ = W₁ · z + b₁
각 뉴런의 계산 예시:
뉴런1 = 0.0762×0.5 + (-0.3221)×(-0.3) + (-0.2734)×0.8 + (-0.3234)
이렇게 5개의 값이 생성됩니다.
두 번째 Linear 레이어 (5 → 4)
W₂ = [[-0.4093, 0.2127, -0.1650, -0.0907, -0.2445],
[-0.3008, 0.2487, -0.3585, 0.3977, 0.3180],
[ 0.2415, -0.4205, -0.3090, 0.2092, 0.2356],
[-0.4201, 0.3128, 0.1191, -0.3206, -0.0462]]
b₂ = [0.2013, -0.0937, 0.1788, -0.0041]
5개의 값에 W₂를 곱하고 b₂를 더하면 4개의 값이 생성됩니다.
Reshape
4개의 값을 2×2 행렬로 변환:
[a, b, c, d] → [[a, b],
[c, d]]
이것이 최종 생성된 이미지입니다!
class Generator(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(100, 128), # 100차원 노이즈 → 128
nn.ReLU(),
nn.Linear(128, 784), # 128 → 784 (28×28)
nn.Tanh() # [-1, 1] 범위로
)
def forward(self, z):
out = self.net(z)
return out.view(-1, 1, 28, 28) # 이미지 형태로 변환
핵심 포인트
class Discriminator(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Flatten(), # 1×28×28 → 784
nn.Linear(784, 128),
nn.LeakyReLU(0.2), # 음수 영역에서도 0.2만큼 활성화
nn.Linear(128, 1),
nn.Sigmoid() # [0, 1] 확률값 출력
)
def forward(self, x):
return self.net(x)
일반 ReLU의 문제점
ReLU(x) = max(0, x)
LeakyReLU의 해결
LeakyReLU(x) = max(0.2x, x)
왜 LeakyReLU를 사용하나?
# 장치 설정
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 모델 초기화
G = Generator().to(device)
D = Discriminator().to(device)
# 손실 함수와 옵티마이저
criterion = nn.BCELoss() # Binary Cross Entropy
opt_G = torch.optim.Adam(G.parameters(), lr=0.0002)
opt_D = torch.optim.Adam(D.parameters(), lr=0.0002)
# 고정된 노이즈 (평가용)
fixed_noise = torch.randn(64, 100, device=device)
핵심: 두 번의 학습 단계만 존재
# ----- 1) Discriminator 학습 -----
z = torch.randn(batch, 100, device=device)
fake = G(z)
pred_real = D(real) # 진짜 이미지 판별
pred_fake = D(fake.detach()) # 가짜 이미지 판별 (detach!)
# 진짜 이미지를 1로 만들기
loss_D_real = criterion(
pred_real,
torch.ones(batch, 1, device=device)
)
# 가짜 이미지를 0으로 만들기
loss_D_fake = criterion(
pred_fake,
torch.zeros(batch, 1, device=device)
)
loss_D = loss_D_real + loss_D_fake
opt_D.zero_grad()
loss_D.backward()
opt_D.step()
detach()를 사용하는 이유
fake = G(z)로 생성된 이미지는 Generator의 그래프에 연결되어 있음detach()로 그래프를 끊어서 Discriminator만 업데이트# ----- 2) Generator 학습 -----
z = torch.randn(batch, 100, device=device)
fake = G(z)
pred_fake = D(fake)
# Generator는 판별자가 1(진짜)로 판단하길 원함
loss_G = criterion(
pred_fake,
torch.ones(batch, 1, device=device)
)
opt_G.zero_grad()
loss_G.backward()
opt_G.step()
핵심 포인트
예시: 간단한 판별자 학습
real_img = torch.tensor([[1.0, 0.0, 1.0]]) # 진짜
fake_img = torch.tensor([[0.2, 0.7, -0.3]]) # 가짜
D = nn.Linear(3, 1, bias=True)
criterion = nn.BCELoss()
opt = optim.SGD(D.parameters(), lr=0.1)
for step in range(5):
pred_real = torch.sigmoid(D(real_img))
pred_fake = torch.sigmoid(D(fake_img))
real_label = torch.ones((1,1)) # 1
fake_label = torch.zeros((1,1)) # 0
loss = criterion(pred_real, real_label) + \
criterion(pred_fake, fake_label)
opt.zero_grad()
loss.backward()
opt.step()
학습 결과 분석
=== STEP 0 ===
pred_real = 0.5154
pred_fake = 0.4748
loss = 1.3068
=== STEP 4 ===
pred_real = 0.6066
pred_fake = 0.4405
loss = 1.0806
결론: 가중치와 편향이 업데이트되면서 판별 능력이 향상됨!
잘못된 방법
fixed_noise = torch.randn(1, 100, device=device) # 1개만 생성
for real, _ in loader:
real = real.to(device) # 배치 크기: 64
batch = real.shape[0] # 64
# 1개의 노이즈로 64개의 실제 이미지를 비교? → 비효율적
올바른 방법
for real, _ in loader:
real = real.to(device)
batch = real.shape[0] # 64
z = torch.randn(batch, 100, device=device) # 배치 크기만큼 생성
fake = G(z) # 64개의 가짜 이미지 생성
이유
fixed_noise = torch.randn(64, 100, device=device)
for epoch in range(100):
generator.train()
discriminator.train()
total_d_loss = 0.0
total_g_loss = 0.0
total_real_score = 0.0
total_fake_score = 0.0
num_batches = 0
for real_images, _ in dataloader:
num_batches += 1
batch_size = real_images.size(0)
# 라벨 준비
real_labels = torch.ones(batch_size, 1).to(device)
fake_labels = torch.zeros(batch_size, 1).to(device)
# ========== Discriminator 학습 ==========
discriminator.zero_grad()
# 진짜 이미지 판별
outputs_real = discriminator(real_images.to(device))
d_loss_real = criterion(outputs_real, real_labels)
real_score = outputs_real.mean().item()
# 가짜 이미지 판별
noise = torch.randn(batch_size, 100, device=device)
fake_images = generator(noise)
outputs_fake = discriminator(fake_images.detach())
d_loss_fake = criterion(outputs_fake, fake_labels)
fake_score = outputs_fake.mean().item()
# Discriminator 전체 손실
d_loss = d_loss_real + d_loss_fake
d_loss.backward()
optimizer_D.step()
# ========== Generator 학습 ==========
generator.zero_grad()
# 새로운 노이즈로 가짜 이미지 생성
noise2 = torch.randn(batch_size, 100, device=device)
fake_images2 = generator(noise2)
outputs = discriminator(fake_images2)
# Generator는 판별자를 속이려고 함 (라벨을 1로)
g_loss = criterion(outputs, real_labels)
g_loss.backward()
optimizer_G.step()
total_d_loss += d_loss.item()
total_g_loss += g_loss.item()
total_real_score += real_score
total_fake_score += fake_score
# 에포크 결과 출력
avg_d_loss = total_d_loss / num_batches
avg_g_loss = total_g_loss / num_batches
avg_real_score = total_real_score / num_batches
avg_fake_score = total_fake_score / num_batches
print(f"Epoch [{epoch+1}/100]: "
f"d_loss={avg_d_loss:.4f}, "
f"g_loss={avg_g_loss:.4f}, "
f"D(x)={avg_real_score:.2f}, "
f"D(G(z))={avg_fake_score:.2f}")
d_loss (Discriminator Loss)
g_loss (Generator Loss)
D(x) (진짜 이미지에 대한 판별자 출력)
D(G(z)) (가짜 이미지에 대한 판별자 출력)
생성 모델의 성능을 평가하는 것은 매우 중요합니다:
1. 생성 품질 (Quality)
2. 다양성 (Diversity)
3. 분포 적합성 (Distribution Matching)
생성된 이미지를 분류하고, 분류된 클래스의 다양성과 확신도를 측정합니다.
IS = exp(𝔼ₓ[KL(p(y|x) || p(y))])
여기서:
높은 IS 값의 의미
예시
나쁜 경우:
이미지1: [0.5, 0.3, 0.2] # 확신도 낮음
이미지2: [0.5, 0.3, 0.2] # 확신도 낮음
이미지3: [0.5, 0.3, 0.2] # 확신도 낮음
→ IS 낮음
좋은 경우:
이미지1: [0.9, 0.05, 0.05] # 클래스 0, 확신도 높음
이미지2: [0.05, 0.9, 0.05] # 클래스 1, 확신도 높음
이미지3: [0.05, 0.05, 0.9] # 클래스 2, 확신도 높음
→ IS 높음 (확신도 높고 다양함)
장점
단점
생성된 데이터 분포와 실제 데이터 분포 간의 유사성을 측정합니다.
두 분포의 평균과 공분산을 비교합니다.
FID = ||μᵣ - μ_g||² + Tr(Σᵣ + Σ_g - 2(ΣᵣΣ_g)^(1/2))
여기서:
첫 번째 항: ||μᵣ - μ_g||²
두 번째 항: Tr(Σᵣ + Σ_g - 2(ΣᵣΣ_g)^(1/2))
낮은 FID 값 = 좋은 성능
예시
실제 데이터: 평균 = [10, 20], 분산 = [5, 5]
생성 데이터: 평균 = [10, 20], 분산 = [5, 5]
→ FID ≈ 0 (완벽하게 일치)
실제 데이터: 평균 = [10, 20], 분산 = [5, 5]
생성 데이터: 평균 = [15, 25], 분산 = [10, 10]
→ FID > 0 (차이 발생)
장점
단점
생성된 데이터의 품질(Precision)과 다양성(Recall)을 분리하여 측정합니다.
Precision (정밀도)
Recall (재현율)
[실제 데이터 분포]
┌─────────────┐
│ · · · │
│ · · · │
│ · · · │
└─────────────┘
[경우 1: High Precision, Low Recall]
┌─────────────┐
│ ○ ○ │ ← 생성된 데이터가 실제 분포 안에만 있음 (Precision 높음)
│ │ 하지만 분포의 일부만 커버 (Recall 낮음)
│ │
└─────────────┘
[경우 2: Low Precision, High Recall]
┌─────────────┐
│ ○ ○ ○ │ ← 실제 분포를 넓게 커버 (Recall 높음)
○ │ ○ ○ ○ │ ○ 하지만 분포 밖에도 생성 (Precision 낮음)
│ ○ ○ │
└─────────────┘
[경우 3: High Precision, High Recall]
┌─────────────┐
│ ○ ○ ○ ○ │ ← 실제 분포 안에 있으면서 (Precision 높음)
│ ○ ○ ○ ○ │ 분포를 넓게 커버 (Recall 높음)
│ ○ ○ ○ ○ │
└─────────────┘
장점
단점
개념
예시
주요 사용 사례
개념
장점
단점
| 지표 | 측정 내용 | 장점 | 단점 |
|---|---|---|---|
| IS | 품질 + 다양성 | 간단, 빠름 | Inception 모델 의존적, 실제 데이터와 비교 안 함 |
| FID | 분포 유사성 | 실제 데이터와 비교, 신뢰성 높음 | 정규분포 가정, 계산 비용 |
| Precision | 품질 (진짜 같은 정도) | 품질만 집중 측정 | Recall과 함께 봐야 함 |
| Recall | 다양성 (커버리지) | 다양성만 집중 측정 | Precision과 함께 봐야 함 |
| PPL | 잠재 공간 매끄러움 | 잠재 공간 품질 평가 | 특정 모델에만 적합 |
| KID | 분포 유사성 (커널 기반) | 작은 데이터셋에서도 안정적 | 계산 복잡도 높음 |
단일 지표의 한계
추천 조합
기본: IS + FID
상세: IS + FID + Precision/Recall
고급: IS + FID + Precision/Recall + PPL
User Study의 중요성
평가 방법
1. 100명에게 진짜/가짜 이미지 섞어서 보여주기
2. 진짜라고 판단한 비율 측정
3. 자동 지표와 비교
의료 영상
예술/디자인
데이터 증강
Batch Normalization
Spectral Normalization
Feature Matching
Gradient Penalty (WGAN-GP)
일반적인 설정
lr_G = 0.0002 # Generator 학습률
lr_D = 0.0002 # Discriminator 학습률
불균형 해결
# Discriminator가 너무 강할 때
lr_G = 0.0002
lr_D = 0.0001 # Discriminator 학습률 낮춤
# Generator가 너무 강할 때
lr_G = 0.0001 # Generator 학습률 낮춤
lr_D = 0.0002
transform = transforms.Compose([
transforms.Resize(28),
transforms.RandomRotation(5), # 회전
transforms.RandomHorizontalFlip(), # 좌우 반전
transforms.ToTensor(),
transforms.Normalize([0.5], [0.5])
])
실제 이미지 데이터를 [-1, 1] 범위로 정규화하기 때문입니다.
transforms.Normalize([0.5], [0.5])
이 변환은 [0, 1] 범위의 데이터를 [-1, 1]로 변환합니다:
x_normalized = (x - 0.5) / 0.5
따라서 Generator의 출력도 같은 범위여야 비교 가능합니다.
Autograd의 연결을 끊습니다.
# detach() 없이
fake = G(z) # G의 그래프에 연결됨
output = D(fake) # D의 그래프에도 연결됨
loss.backward() # G와 D 모두 업데이트됨 (원하지 않음!)
# detach() 사용
fake = G(z)
output = D(fake.detach()) # D의 그래프에만 연결됨
loss.backward() # D만 업데이트됨 (올바름!)
기술적으로는 동작하지만 성능이 떨어집니다:
GAN은 수렴이 느립니다:
인내심을 가지고 충분히 학습시켜야 합니다!
아닙니다. IS는 한계가 있습니다:
GAN은 Generator와 Discriminator가 서로 경쟁하며 학습한다.
Generator는 노이즈에서 시작해 신경망을 통해 이미지를 생성한다.
Discriminator는 진짜 이미지는 1로, 가짜 이미지는 0으로 판별한다.
배치 크기를 맞춰서 각 이미지를 독립적으로 비교해야 한다.
detach()는 Discriminator 학습 시 Generator의 그래프를 끊기 위해 사용한다.
Inception Score는 품질과 다양성을 동시에 측정한다.
FID는 실제 데이터와 생성 데이터의 분포 차이를 측정한다.
Precision/Recall은 품질과 다양성을 분리하여 측정한다.
여러 평가지표를 조합하여 사용하는 것이 좋다.
GAN 학습은 불안정하므로 Spectral Normalization, WGAN 등의 기법을 사용한다.
1. 고급 GAN 아키텍처
2. 평가지표 심화
3. 최적화 기법
4. 실무 응용
프로젝트 1: MNIST GAN 완성하기
프로젝트 2: Fashion-MNIST GAN
프로젝트 3: 조건부 GAN
프로젝트 4: 고해상도 얼굴 생성
논문
온라인 자료
도구 및 라이브러리
이론과 실습을 병행하세요. 수식만 보면 어렵지만 코드로 보면 이해가 쉽습니다.
작은 데이터셋부터 시작하세요. MNIST → Fashion-MNIST → CelebA 순서로.
학습 과정을 시각화하세요. TensorBoard를 사용하면 손실과 이미지를 실시간으로 확인할 수 있습니다.
여러 평가지표를 계산해보세요. 수치로 보면 모델의 성능을 객관적으로 이해할 수 있습니다.
인내심을 가지세요. GAN은 학습이 느리고 불안정합니다. 충분한 에포크를 돌려야 합니다.