파이토치를 활용한 GAN 딥러닝, 강화학습

1. 소개

Generative Adversarial Networks (GANs)는 2014년 Ian Goodfellow에 의해 제안된 모델로, 두 신경망 간의 경쟁을 통해 데이터를 생성하는 모델입니다. GAN은 특히 이미지 생성, 스타일 변환, 데이터 증강 등에 널리 사용되고 있습니다. 이번 포스팅에서는 GAN의 기본 구조, 파이토치를 이용한 구현 방법, 강화학습의 기본 개념 및 여러 응용 사례를 소개하겠습니다.

2. GAN의 기본 구조

GAN은 두 개의 신경망으로 구성됩니다: 생성자(Generator)와 판별자(Discriminator)입니다. 생성자는 무작위 노이즈를 입력으로 받아 새로운 데이터를 생성하고, 판별자는 입력 데이터가 진짜 데이터인지 생성된 데이터인지를 구별합니다. 이 두 네트워크는 서로 경쟁하면서 학습합니다.

2.1 생성자 (Generator)

생성자는 노이즈 벡터를 받아 진짜처럼 보이는 데이터를 생성합니다. 목표는 판별자를 속이는 것입니다.

2.2 판별자 (Discriminator)

판별자는 입력 데이터의 진위 여부를 판단합니다. 진짜 데이터일 경우 1, 생성된 데이터일 경우 0을 출력합니다.

2.3 GAN의 손실 함수

GAN의 손실 함수는 다음과 같이 설정됩니다:

min_G max_D V(D, G) = E[log(D(x))] + E[log(1 - D(G(z)))]

여기서 E는 기대값을 나타내며, x는 진짜 데이터, G(z)는 생성자가 생성한 데이터입니다. 생성자는 손실을 최소화하려고 하고, 판별자는 손실을 최대화하려 하고 있습니다.

3. 파이토치를 활용한 GAN 구현

이제 GAN을 파이토치로 구현해 보겠습니다. 데이터셋으로는 MNIST 손글씨 숫자 데이터를 사용할 것입니다.

3.1 데이터셋 준비

import torch
import torchvision
from torchvision import datasets, transforms

# 데이터 변환 및 다운로드
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

# MNIST 데이터셋
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=64, shuffle=True)

3.2 생성자 (Generator) 모델 정의

import torch.nn as nn

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Linear(100, 256),
            nn.ReLU(True)
        )
        self.layer2 = nn.Sequential(
            nn.Linear(256, 512),
            nn.ReLU(True)
        )
        self.layer3 = nn.Sequential(
            nn.Linear(512, 1024),
            nn.ReLU(True)
        )
        self.layer4 = nn.Sequential(
            nn.Linear(1024, 28*28),
            nn.Tanh()  # 픽셀 값은 -1과 1 사이
        )
    
    def forward(self, z):
        out = self.layer1(z)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        return out.view(-1, 1, 28, 28)  # 이미지 형태로 변환

3.3 판별자 (Discriminator) 모델 정의

class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Linear(28*28, 1024),
            nn.LeakyReLU(0.2, inplace=True)
        )
        self.layer2 = nn.Sequential(
            nn.Linear(1024, 512),
            nn.LeakyReLU(0.2, inplace=True)
        )
        self.layer3 = nn.Sequential(
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2, inplace=True)
        )
        self.layer4 = nn.Sequential(
            nn.Linear(256, 1),
            nn.Sigmoid()  # 출력값을 0과 1 사이로
        )
    
    def forward(self, x):
        out = self.layer1(x.view(-1, 28*28))  # 평탄화
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.layer4(out)
        return out

3.4 모델 훈련

import torch.optim as optim

# 모델 초기화
generator = Generator()
discriminator = Discriminator()

# 손실 함수 및 최적화기 설정
criterion = nn.BCELoss()  # Binary Cross Entropy Loss
optimizer_g = optim.Adam(generator.parameters(), lr=0.0002)
optimizer_d = optim.Adam(discriminator.parameters(), lr=0.0002)

# 훈련
num_epochs = 200
for epoch in range(num_epochs):
    for i, (images, _) in enumerate(train_loader):
        # 진짜 데이터 레이블
        real_labels = torch.ones(images.size(0), 1)
        fake_labels = torch.zeros(images.size(0), 1)

        # 판별자 훈련
        optimizer_d.zero_grad()
        outputs = discriminator(images)
        d_loss_real = criterion(outputs, real_labels)
        d_loss_real.backward()
        
        z = torch.randn(images.size(0), 100)
        fake_images = generator(z)
        outputs = discriminator(fake_images.detach())
        d_loss_fake = criterion(outputs, fake_labels)
        d_loss_fake.backward()
        
        optimizer_d.step()
        
        # 생성자 훈련
        optimizer_g.zero_grad()
        outputs = discriminator(fake_images)
        g_loss = criterion(outputs, real_labels)
        g_loss.backward()
        optimizer_g.step()
    
    if (epoch+1) % 10 == 0:
        print(f'Epoch [{epoch+1}/{num_epochs}], d_loss: {d_loss_real.item() + d_loss_fake.item():.4f}, g_loss: {g_loss.item():.4f}')

3.5 결과 시각화

import matplotlib.pyplot as plt

# 생성된 이미지 시각화 함수
def plot_generated_images(generator, n=10):
    z = torch.randn(n, 100)
    with torch.no_grad():
        generated_images = generator(z).cpu()
    generated_images = generated_images.view(-1, 28, 28)
    
    plt.figure(figsize=(10, 1))
    for i in range(n):
        plt.subplot(1, n, i+1)
        plt.imshow(generated_images[i], cmap='gray')
        plt.axis('off')
    plt.show()

# 이미지 생성
plot_generated_images(generator)

4. 강화학습의 기본 개념

강화학습(Reinforcement Learning, RL)은 에이전트가 환경과 상호작용하며 최적의 행동을 학습하는 기계 학습의 한 분야입니다. 에이전트는 상태를 관찰하고, 행동을 선택하고, 보상을 받으며, 이를 통해 최적의 정책을 학습합니다.

4.1 강화학습의 구성 요소

  • 상태 (State): 에이전트가 현재의 환경을 나타내는 정보입니다.
  • 행동 (Action): 에이전트가 현재 상태에서 수행할 수 있는 작업입니다.
  • 보상 (Reward): 에이전트가 행동을 수행한 후에 환경으로부터 받는 피드백입니다.
  • 정책 (Policy): 에이전트가 각 상태에서 취할 행동의 확률 분포를 나타냅니다.

4.2 강화학습 알고리즘

  • Q-Learning: 가치 기반 방법으로, Q 값을 학습하여 최적의 정책을 유도합니다.
  • 정책 경사 방법 (Policy Gradient): 정책을 직접 학습하는 방법입니다.
  • Actor-Critic: 가치 함수와 정책을 동시에 학습하는 방법입니다.

4.3 파이토치를 활용한 강화학습 구현

간단한 강화학습 구현을 위해 OpenAI의 Gym 라이브러리를 사용할 것입니다. 여기서는 CartPole 환경을 다루겠습니다.

4.3.1 Gym 환경 설정

import gym

# Gym 환경 생성
env = gym.make('CartPole-v1')  # CartPole 환경

4.3.2 DQN 모델 정의

class DQN(nn.Module):
    def __init__(self, input_size, num_actions):
        super(DQN, self).__init__()
        self.fc1 = nn.Linear(input_size, 24)
        self.fc2 = nn.Linear(24, 24)
        self.fc3 = nn.Linear(24, num_actions)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        return self.fc3(x)

4.3.3 모델 훈련

def train_dqn(env, num_episodes):
    model = DQN(input_size=env.observation_space.shape[0], num_actions=env.action_space.n)
    optimizer = optim.Adam(model.parameters())
    criterion = nn.MSELoss()

    for episode in range(num_episodes):
        state = env.reset()
        state = torch.FloatTensor(state)
        done = False
        total_reward = 0

        while not done:
            q_values = model(state)
            action = torch.argmax(q_values).item()  # 또는 epsilon-greedy 정책 사용

            next_state, reward, done, _ = env.step(action)
            next_state = torch.FloatTensor(next_state)

            total_reward += reward

            # DQN 업데이트 로직 추가 필요

            state = next_state

        print(f'Episode {episode+1}, Total Reward: {total_reward}')  

    return model

# DQN 훈련 시작
train_dqn(env, num_episodes=1000)

5. 결론

이번 포스팅에서는 GAN과 강화학습의 기본 개념 및 파이토치를 활용한 구현 방법에 대해 알아보았습니다. GAN은 데이터 생성에 매우 유용한 모델이고, 강화학습은 에이전트가 최적의 정책을 학습하도록 돕는 기법입니다. 이러한 기술들은 다양한 분야에서 응용될 수 있으며, 앞으로의 연구와 발전이 기대됩니다.

6. 참고 자료

파이토치를 활용한 GAN 딥러닝, WGAN-GP

Generative Adversarial Networks(이하 GAN)는 Ian Goodfellow가 2014년에 제안한 강력한 생성 모델입니다. GAN은 두 개의 신경망, 즉 생성자(Generator)와 판별자(Discriminator)로 구성되며, 이 두 네트워크는 서로 경쟁하여 학습합니다. 생성자는 실제 데이터와 유사한 새로운 데이터를 생성하려고 하고, 판별자는 주어진 데이터가 실제 데이터인지 생성된 데이터인지를 구별하려고 합니다. 이들은 지속적으로 개선되면서 최종적으로 매우 평범한 데이터를 신뢰성 있게 생성할 수 있게 됩니다.

본 글에서는 GAN의 변형인 Wasserstein GAN with Gradient Penalty (WGAN-GP)에 대해 설명하고, 파이토치를 사용하여 WGAN-GP를 구현해보겠습니다. WGAN-GP는 Wasserstein 거리(Wasserstein Distance)를 기반으로 하며, 판별자에서의 Gradient Penalty를 추가하여 훈련의 안정성을 높입니다.

1. GAN의 기본 구조

GAN의 기본 구조는 다음과 같습니다.

  • 생성자(Generator): 랜덤 노이즈를 입력받아 가짜 데이터를 생성합니다.
  • 판별자(Discriminator): 실제 데이터와 생성자가 만든 가짜 데이터를 입력받아 이 둘이 얼마나 유사한지를 판단합니다.

GAN의 학습 과정은 다음의 두 단계로 이루어집니다.

이 과정은 반복적으로 수행되어 두 네트워크 모두 개선됩니다. 다만, 기존 GAN은 훈련 불안정성과 모드 붕괴(mode collapse) 문제가 종종 발생하여 보다 안정적인 학습을 위한 여러 접근법이 연구되었습니다.

2. WGAN-GP 소개

WGAN에서는 Wasserstein 거리 개념을 도입하여 GAN의 본질적인 문제를 해결하고자 하였습니다. Wasserstein 거리는 두 분포 간의 차이를 보다 명확히 정의할 수 있어, 네트워크를 훈련하는 데 유리합니다. WGAN의 핵심 아이디어는 판별자가 아닌 “비평가(critic)”라는 개념을 도입하는 것입니다. 비평가는 생성된 데이터와 실제 데이터 간의 거리를 평가하고, 이 평가 결과를 바탕으로 평균 제곱 오차(MSE) 손실이 아닌 Wasserstein 손실을 사용하여 네트워크를 업데이트합니다.

WGAN에서 Gradient Penalty (GP)를 추가함으로써 판별자의 Lipschitz 조건을 준수할 수 있도록 하여 훈련의 안정성을 더욱 높였습니다. Gradient Penalty는 다음과 같이 정의됩니다:

GP = λ * E[(||∇D(x) ||2 - 1)²]

여기서 λ는 하이퍼파라미터이며, D(x)는 판별자의 출력입니다. Gradient Penalty를 통해 판별자의 기울기가 1로 유지되도록 강화할 수 있습니다. 이 방식을 통해 WGAN-GP는 GAN의 불안정을 극복하고 보다 안정적인 훈련이 가능해집니다.

3. WGAN-GP 파이토치 구현

이제 파이토치를 사용하여 WGAN-GP를 구현해보겠습니다. 다음의 단계로 진행됩니다:

  1. 필요한 라이브러리 설치 및 데이터셋 불러오기
  2. 생성자 및 판별자 모델 정의
  3. WGAN-GP 훈련 루프 구현
  4. 결과 시각화

3.1 라이브러리 설치 및 데이터셋 불러오기

우선 필요한 라이브러리를 설치하고, MNIST 데이터셋을 불러옵니다.

!pip install torch torchvision matplotlib
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.datasets as dsets
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import numpy as np

3.2 생성자 및 판별자 모델 정의

생성자와 판별자 모델을 정의합니다. 생성자는 랜덤한 노이즈 벡터를 입력으로 받아 이미지로 변환하고, 판별자는 입력된 이미지를 바탕으로 진짜인지 가짜인지를 평가합니다.

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.model = nn.Sequential(
            nn.Linear(100, 256),
            nn.ReLU(True),
            nn.Linear(256, 512),
            nn.ReLU(True),
            nn.Linear(512, 1024),
            nn.ReLU(True),
            nn.Linear(1024, 28 * 28),
            nn.Tanh()
        )
    
    def forward(self, z):
        return self.model(z).reshape(-1, 1, 28, 28)

class Critic(nn.Module):
    def __init__(self):
        super(Critic, self).__init__()
        self.model = nn.Sequential(
            nn.Linear(28 * 28, 1024),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(1024, 512),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(256, 1)
        )

    def forward(self, x):
        return self.model(x.view(-1, 28 * 28))

3.3 WGAN-GP 훈련 루프 구현

이제 WGAN-GP의 훈련 루프를 구현합니다. 훈련 과정에서는 판별자를 일정 횟수만큼 업데이트한 후 생성자를 업데이트합니다. Gradient Penalty도 손실에 포함됩니다.

def compute_gradient_penalty(critic, real_samples, fake_samples):
    alpha = torch.rand(real_samples.size(0), 1, 1, 1).expand_as(real_samples)
    interpolated_samples = alpha * real_samples + (1 - alpha) * fake_samples
    interpolated_samples.requires_grad_(True)

    d_interpolated = critic(interpolated_samples)

    gradients = torch.autograd.grad(outputs=d_interpolated, inputs=interpolated_samples,
                                    grad_outputs=torch.ones_like(d_interpolated),
                                    create_graph=True, retain_graph=True)[0]

    gradient_penalty = ((gradients.norm(2, dim=1) - 1) ** 2).mean()
    return gradient_penalty
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

generator = Generator().to(device)
critic = Critic().to(device)

learning_rate = 0.00005
num_epochs = 100
critic_iterations = 5
lambda_gp = 10

criterion = nn.MSELoss()
optimizer_generator = optim.Adam(generator.parameters(), lr=learning_rate)
optimizer_critic = optim.Adam(critic.parameters(), lr=learning_rate)

Real_data = dsets.MNIST(root='./data', train=True, transform=transforms.ToTensor(), download=True)

data_loader = torch.utils.data.DataLoader(Real_data, batch_size=64, shuffle=True)

for epoch in range(num_epochs):
    for i, (real_images, _) in enumerate(data_loader):
        real_images = real_images.to(device)

        for _ in range(critic_iterations):
            optimizer_critic.zero_grad()

            # Generate fake images
            z = torch.randn(real_images.size(0), 100).to(device)
            fake_images = generator(z)

            # Get critic scores
            real_validity = critic(real_images)
            fake_validity = critic(fake_images)
            gradient_penalty = compute_gradient_penalty(critic, real_images.data, fake_images.data)

            # Compute loss
            critic_loss = -torch.mean(real_validity) + torch.mean(fake_validity) + lambda_gp * gradient_penalty
            critic_loss.backward()
            optimizer_critic.step()

        # Update generator
        optimizer_generator.zero_grad()
        
        # Get generator score
        fake_images = generator(z)
        validity = critic(fake_images)
        generator_loss = -torch.mean(validity)
        generator_loss.backward()
        optimizer_generator.step()

    if epoch % 10 == 0:
        print(f"Epoch: {epoch}/{num_epochs}, Critic Loss: {critic_loss.item():.4f}, Generator Loss: {generator_loss.item():.4f}")

3.4 결과 시각화

최종적으로 생성된 이미지를 시각화합니다. 이는 학습 과정에서 생성자가 얼마나 잘 학습되었는지를 확인하는 좋은 방법입니다.

def show_generated_images(generator, num_images=25):
    z = torch.randn(num_images, 100).to(device)
    generated_images = generator(z).cpu().detach().numpy()

    plt.figure(figsize=(5, 5))
    for i in range(num_images):
        plt.subplot(5, 5, i + 1)
        plt.imshow(generated_images[i][0], cmap='gray')
        plt.axis('off')
    plt.show()

show_generated_images(generator)

4. 결론

이 글에서는 GAN의 변형 모델인 WGAN-GP에 대해 설명하고, 파이토치를 활용하여 WGAN-GP를 구현해보았습니다. WGAN-GP는 Wasserstein 거리와 Gradient Penalty를 활용하여 보다 안정적인 훈련이 가능하다는 장점을 가지고 있습니다. 이러한 GAN 계열 모델은 이미지 생성, 이미지 변환, 스타일 전이 등 다양한 분야에 활용될 수 있습니다.

딥러닝의 발전과 함께 GAN과 그 변형 모델들은 계속해서 주목받고 있으며, 앞으로의 발전이 기대됩니다. 여러분도 GAN 및 WGAN-GP를 활용하여 다양한 프로젝트에 도전해보시기를 바랍니다!

파이토치를 활용한 GAN 딥러닝, VAE를 사용하여 얼굴 이미지 생성

최근 인공지능 분야에서 생성적 적대 신경망(GAN)과 변형 오토인코더(VAE)는 이미지 생성의 효율과 품질을 크게 향상시키는 중요한 기술로 자리 잡았습니다. 이 글에서는 GAN과 VAE의 기본 개념과 함께, 파이토치를 사용하여 얼굴 이미지를 생성하는 과정을 자세히 살펴보겠습니다.

1. GAN(Generative Adversarial Networks) 개요

Generative Adversarial Networks(GAN)는 두 개의 신경망인 생성자(Generator)와 판별자(Discriminator)가 서로 경쟁하며 학습하는 구조를 가지고 있습니다. 생성자는 실제와 유사한 이미지를 생성하려고 하고, 판별자는 생성된 이미지가 진짜인지 가짜인지를 판별합니다. 이 과정은 생성자가 판별자를 속이도록 학습하면서 점점 더 현실적인 이미지를 생성하는 데 도움을 주게 됩니다.

1.1 GAN의 동작 원리

GAN은 다음과 같은 두 가지의 네트워크로 구성됩니다:

  • 생성자(Generator): 랜덤 노이즈를 입력으로 받아 실제와 유사한 이미지를 생성합니다.
  • 판별자(Discriminator): 입력된 이미지가 진짜인지 가짜인지 분류합니다.

학습이 진행됨에 따라 생성자는 점차 품질 높은 이미지를 생성하고, 판별자는 더욱 정확하게 이미지를 분석합니다. 이 과정은 제로섬 게임의 형태로 진행되며, GAN 모델의 목표는 이 두 네트워크의 성능을 동시에 향상시키는 것입니다.

2. VAE(Variational Autoencoder) 개요

변형 오토인코더(VAE)는 이미지나 데이터의 잠재 공간(latent space)을 학습하여 새로운 데이터를 생성할 수 있도록 하는 모델입니다. VAE는 입력 데이터를 인코더를 통해 저차원 잠재 공간으로 변환한 후, 이 잠재 공간에서 샘플링하여 디코더를 사용하여 이미지를 재구성합니다. VAE는 확률적 모델로, 입력 데이터의 분포를 학습하고 이를 기반으로 새로운 샘플을 생성합니다.

2.1 VAE의 구조

VAE는 다음과 같은 세 가지 주요 구성 요소로 이루어집니다:

  • 인코더(Encoder): 입력 데이터를 잠재 변수로 변환합니다.
  • 샘플링(Sampling): 잠재 변수에서 샘플을 추출합니다.
  • 디코더(Decoder): 샘플된 잠재 변수를 사용하여 새로운 이미지를 생성합니다.

3. 프로젝트 목표와 데이터셋

이번 프로젝트의 목표는 GAN 및 VAE를 사용하여 실제와 유사한 얼굴 이미지를 생성하는 것입니다. 이를 위해 CelebA 데이터셋을 사용하겠습니다. CelebA 데이터셋에는 다양한 얼굴 이미지가 포함되어 있으며, GAN과 VAE의 성능을 측정하는 데 적합한 데이터셋입니다.

4. 환경 설정

이 프로젝트를 진행하기 위해 Python과 PyTorch 프레임워크가 필요합니다. 다음은 필요한 패키지 목록입니다:

pip install torch torchvision matplotlib

5. 파이토치로 GAN 구현하기

먼저 GAN 모델을 구현해 보겠습니다. GAN의 구조는 다음과 같은 단계로 구성됩니다:

  • 데이터셋 로딩
  • 생성자와 판별자 정의
  • 훈련 루프 설정
  • 결과 시각화

5.1 데이터셋 로딩

우선, CelebA 데이터셋을 다운로드하고 준비합니다.

import torchvision.transforms as transforms
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader

transform = transforms.Compose([
    transforms.Resize((64, 64)),
    transforms.ToTensor(),
])

dataset = ImageFolder(root='path_to_celeba', transform=transform)
dataloader = DataLoader(dataset, batch_size=64, shuffle=True)

5.2 생성자 및 판별자 정의

GAN의 생성자 및 판별자를 정의합니다.

import torch
import torch.nn as nn

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.model = nn.Sequential(
            nn.Linear(100, 256),
            nn.ReLU(),
            nn.Linear(256, 512),
            nn.ReLU(),
            nn.Linear(512, 1024),
            nn.ReLU(),
            nn.Linear(1024, 3 * 64 * 64),
            nn.Tanh(),
        )

    def forward(self, z):
        z = self.model(z)
        return z.view(-1, 3, 64, 64)

class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.model = nn.Sequential(
            nn.Linear(3 * 64 * 64, 512),
            nn.LeakyReLU(0.2),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2),
            nn.Linear(256, 1),
            nn.Sigmoid(),
        )

    def forward(self, img):
        img_flat = img.view(img.size(0), -1)
        return self.model(img_flat)

5.3 훈련 루프 설정

이제 GAN의 훈련 과정을 구현합니다.

import torch.optim as optim

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
generator = Generator().to(device)
discriminator = Discriminator().to(device)

criterion = nn.BCELoss()
g_optimizer = optim.Adam(generator.parameters(), lr=0.0002, betas=(0.5, 0.999))
d_optimizer = optim.Adam(discriminator.parameters(), lr=0.0002, betas=(0.5, 0.999))

num_epochs = 50
for epoch in range(num_epochs):
    for i, (imgs, _) in enumerate(dataloader):
        imgs = imgs.to(device)
        batch_size = imgs.size(0)

        # 레이블 설정
        real_labels = torch.ones(batch_size, 1).to(device)
        fake_labels = torch.zeros(batch_size, 1).to(device)

        # 판별자 훈련
        d_optimizer.zero_grad()
        outputs = discriminator(imgs)
        d_loss_real = criterion(outputs, real_labels)
        d_loss_real.backward()

        z = torch.randn(batch_size, 100).to(device)
        fake_imgs = generator(z)
        outputs = discriminator(fake_imgs.detach())
        d_loss_fake = criterion(outputs, fake_labels)
        d_loss_fake.backward()
        
        d_loss = d_loss_real + d_loss_fake
        d_optimizer.step()

        # 생성자 훈련
        g_optimizer.zero_grad()
        outputs = discriminator(fake_imgs)
        g_loss = criterion(outputs, real_labels)
        g_loss.backward()
        g_optimizer.step()

    print(f'Epoch [{epoch+1}/{num_epochs}], d_loss: {d_loss.item()}, g_loss: {g_loss.item()}')

5.4 결과 시각화

훈련된 생성자가 생성한 이미지를 시각화합니다.

import matplotlib.pyplot as plt

z = torch.randn(64, 100).to(device)
fake_images = generator(z).detach().cpu()

plt.figure(figsize=(8, 8))
for i in range(64):
    plt.subplot(8, 8, i + 1)
    plt.imshow(fake_images[i].permute(1, 2, 0).numpy() * 0.5 + 0.5)
    plt.axis('off')
plt.show()

6. 파이토치로 VAE 구현하기

이제 VAE를 구현해 보겠습니다. VAE의 구조는 GAN과 비슷하지만, 확률적인 접근 방식을 사용합니다. VAE의 구현 단계는 아래와 같습니다:

  • 데이터셋 준비
  • 인코더와 디코더 정의
  • 훈련 루프 설정
  • 결과 시각화

6.1 데이터셋 준비

데이터셋은 GAN을 사용할 때와 동일하게 로드합니다.

6.2 인코더와 디코더 정의

VAE의 인코더와 디코더를 정의합니다.

class VAE(nn.Module):
    def __init__(self):
        super(VAE, self).__init__()
        self.encoder = nn.Sequential(
            nn.Conv2d(3, 16, 4, stride=2, padding=1),
            nn.ReLU(),
            nn.Conv2d(16, 32, 4, stride=2, padding=1),
            nn.ReLU(),
            nn.Conv2d(32, 64, 4, stride=2, padding=1),
            nn.ReLU(),
        )
        self.fc_mu = nn.Linear(64 * 8 * 8, 128)
        self.fc_logvar = nn.Linear(64 * 8 * 8, 128)
        self.fc_decode = nn.Linear(128, 64 * 8 * 8)
        self.decoder = nn.Sequential(
            nn.ConvTranspose2d(64, 32, 4, stride=2, padding=1),
            nn.ReLU(),
            nn.ConvTranspose2d(32, 16, 4, stride=2, padding=1),
            nn.ReLU(),
            nn.ConvTranspose2d(16, 3, 4, stride=2, padding=1),
            nn.Sigmoid(),
        )

    def encode(self, x):
        h = self.encoder(x)
        h = h.view(h.size(0), -1)
        return self.fc_mu(h), self.fc_logvar(h)

    def reparameterize(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std

    def decode(self, z):
        z = self.fc_decode(z).view(-1, 64, 8, 8)
        return self.decoder(z)

    def forward(self, x):
        mu, logvar = self.encode(x)
        z = self.reparameterize(mu, logvar)
        return self.decode(z), mu, logvar

6.3 훈련 루프 설정

VAE의 훈련 과정을 구현합니다. VAE는 두 가지 손실을 사용하여 훈련되며, 원본 이미지와 복원된 이미지 간의 차이(재구성 손실)와 잠재 공간의 분포와 정규 분포 간의 차이(쿨백-라이블러 발산 손실)로 구성됩니다.

vae = VAE().to(device)
optimizer = optim.Adam(vae.parameters(), lr=0.0002)

num_epochs = 50
for epoch in range(num_epochs):
    for imgs, _ in dataloader:
        imgs = imgs.to(device)

        optimizer.zero_grad()
        reconstructed, mu, logvar = vae(imgs)

        re_loss = nn.functional.binary_cross_entropy(reconstructed.view(-1, 3 * 64 * 64), imgs.view(-1, 3 * 64 * 64), reduction='sum')
        kl_loss = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
        loss = re_loss + kl_loss

        loss.backward()
        optimizer.step()

    print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item()}')

6.4 결과 시각화

훈련된 VAE를 사용하여 이미지를 복원하고 시각화합니다.

with torch.no_grad():
    z = torch.randn(64, 128).to(device)
    generated_images = vae.decode(z).cpu()

plt.figure(figsize=(8, 8))
for i in range(64):
    plt.subplot(8, 8, i + 1)
    plt.imshow(generated_images[i].permute(1, 2, 0).numpy())
    plt.axis('off')
plt.show()

7. 결론

이 글에서는 파이토치를 활용하여 GAN과 VAE를 사용하여 얼굴 이미지를 생성하는 방법을 살펴보았습니다. GAN은 생성자와 판별자가 서로 경쟁하면서 점점 더 현실적인 이미지를 생성하도록 학습하는 반면, VAE는 잠재 공간의 분포를 학습하여 새로운 이미지를 생성합니다. 두 기술 모두 이미지 생성 분야에서 중요한 역할을 하고 있으며, 각기 다른 방법으로 놀라운 결과를 만들어낼 수 있습니다.

8. 추가 참고 자료

파이토치를 활용한 GAN 딥러닝, VAE 만들기

1. 서론

인공지능의 발전과 함께 생성 모델(Generative Models)의 중요성이 커지고 있습니다. 생성 모델은 구조적으로 서로 다른 데이터를 생성하는 역할을 하며, 특히 GAN(Generative Adversarial Networks)과 VAE(Variational Autoencoder)가 널리 사용됩니다. 이 글에서는 파이토치(PyTorch)를 활용하여 GAN과 VAE를 구현하는 방법을 자세히 설명하겠습니다.

2. GAN(Generative Adversarial Networks)

GAN은 Ian Goodfellow가 2014년에 제안한 모델로, 두 개의 신경망(생성자와 판별자)이 서로 경쟁하며 학습합니다. 생성자는 가짜 데이터를 만들어내고, 판별자는 진짜 데이터와 가짜 데이터를 구분하는 역할을 합니다.

2.1 GAN의 구조

GAN은 다음과 같은 구조로 구성됩니다:

  • 생성자(Generator): 랜덤 노이즈를 입력으로 받아 진짜 데이터처럼 높은 품질의 가짜 데이터를 생성합니다.
  • 판별자(Discriminator): 입력 데이터를 보고 이 데이터가 진짜인지 가짜인지 판단합니다.

2.2 GAN 학습 과정

GAN의 학습 과정은 다음과 같은 단계를 포함합니다.

  1. 생성자는 랜덤 노이즈를 생성하여 가짜 데이터를 생성합니다.
  2. 판별자는 생성된 가짜 데이터와 진짜 데이터를 입력받아 각 클래스의 확률을 출력합니다.
  3. 생성자는 판별자가 가짜 데이터를 진짜라고 판단하도록 손실(loss)을 최소화하려고 합니다.
  4. 판별자는 진짜 데이터에 대해 높은 확률을, 가짜 데이터에 대해서는 낮은 확률을 출력하도록 손실을 최소화합니다.

2.3 GAN 구현 코드

다음은 간단한 GAN을 구현하는 파이썬 코드입니다:


import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms

# 생성자 클래스 정의
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.model = nn.Sequential(
            nn.Linear(100, 256),
            nn.ReLU(inplace=True),
            nn.Linear(256, 512),
            nn.ReLU(inplace=True),
            nn.Linear(512, 1024),
            nn.ReLU(inplace=True),
            nn.Linear(1024, 784),
            nn.Tanh()
        )

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

# 판별자 클래스 정의
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.model = nn.Sequential(
            nn.Linear(784, 512),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(512, 256),
            nn.LeakyReLU(0.2, inplace=True),
            nn.Linear(256, 1),
            nn.Sigmoid()
        )

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

# 데이터 로딩
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize([0.5], [0.5])
])
mnist = datasets.MNIST('data', train=True, download=True, transform=transform)
dataloader = torch.utils.data.DataLoader(mnist, batch_size=64, shuffle=True)

# 모델 초기화
generator = Generator()
discriminator = Discriminator()

# 손실 함수 및 최적화 알고리즘 설정
criterion = nn.BCELoss()
optimizer_G = optim.Adam(generator.parameters(), lr=0.0002, betas=(0.5, 0.999))
optimizer_D = optim.Adam(discriminator.parameters(), lr=0.0002, betas=(0.5, 0.999))

# GAN 학습
num_epochs = 100
for epoch in range(num_epochs):
    for i, (imgs, _) in enumerate(dataloader):
        # 실제 데이터 레이블 및 가짜 데이터 레이블 설정
        real_labels = torch.ones(imgs.size(0), 1)
        fake_labels = torch.zeros(imgs.size(0), 1)

        # 판별자 학습
        optimizer_D.zero_grad()
        outputs = discriminator(imgs.view(imgs.size(0), -1))
        d_loss_real = criterion(outputs, real_labels)
        d_loss_real.backward()

        z = torch.randn(imgs.size(0), 100)
        fake_imgs = generator(z)
        outputs = discriminator(fake_imgs.detach())
        d_loss_fake = criterion(outputs, fake_labels)
        d_loss_fake.backward()
        optimizer_D.step()

        # 생성자 학습
        optimizer_G.zero_grad()
        outputs = discriminator(fake_imgs)
        g_loss = criterion(outputs, real_labels)
        g_loss.backward()
        optimizer_G.step()

    print(f'Epoch [{epoch+1}/{num_epochs}], d_loss: {d_loss_real.item() + d_loss_fake.item()}, g_loss: {g_loss.item()}')
        

3. VAE(Variational Autoencoder)

VAE는 D. P. Kingma와 M. Welling이 2013년에 제안한 모델로, 확률적 방식으로 데이터를 생성합니다. VAE는 인코더와 디코더로 구성되어 있으며, 인코더는 데이터를 잠재 공간(latent space)으로 압축하고, 디코더는 이 latent space로부터 데이터를 재구성합니다.

3.1 VAE의 구조

VAE의 주요 구성 요소는 다음과 같습니다:

  • 인코더(Encoder): 입력 데이터를 잠재 벡터(latent vector)로 변환하며, 이 벡터는 정규분포를 따르도록 학습됩니다.
  • 디코더(Decoder): 잠재 벡터를 입력 받아 원본 데이터와 비슷한 출력을 생성합니다.

3.2 VAE 학습 과정

VAE의 학습 과정은 다음과 같습니다.

  1. 데이터를 인코더에 통과시켜 평균과 분산을 얻습니다.
  2. 재파라미터화 기법을 사용하여 샘플링합니다.
  3. 샘플링한 잠재 벡터를 디코더에 통과시켜 데이터를 재구성합니다.
  4. 재구성된 데이터와 원본 데이터 간의 손실을 계산합니다.

3.3 VAE 구현 코드

다음은 간단한 VAE를 구현하는 파이썬 코드입니다:


class VAE(nn.Module):
    def __init__(self):
        super(VAE, self).__init__()
        self.encoder = nn.Sequential(
            nn.Linear(784, 400),
            nn.ReLU()
        )
        self.fc_mu = nn.Linear(400, 20)
        self.fc_logvar = nn.Linear(400, 20)
        self.decoder = nn.Sequential(
            nn.Linear(20, 400),
            nn.ReLU(),
            nn.Linear(400, 784),
            nn.Sigmoid()
        )

    def reparametrize(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std

    def forward(self, x):
        h1 = self.encoder(x.view(-1, 784))
        mu = self.fc_mu(h1)
        logvar = self.fc_logvar(h1)
        z = self.reparametrize(mu, logvar)
        return self.decoder(z), mu, logvar

# VAE 학습
vae = VAE()
optimizer = optim.Adam(vae.parameters(), lr=0.001)
criterion = nn.BCELoss(reduction='sum')

num_epochs = 10
for epoch in range(num_epochs):
    for imgs, _ in dataloader:
        optimizer.zero_grad()
        recon_batch, mu, logvar = vae(imgs)
        recon_loss = criterion(recon_batch, imgs.view(-1, 784))
        # Kullback-Leibler divergence
        kld = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
        loss = recon_loss + kld
        loss.backward()
        optimizer.step()
    print(f'Epoch [{epoch+1}/{num_epochs}], Loss: {loss.item()}')
        

4. 결론

GAN과 VAE는 각각 고유한 장점이 있으며, 다양한 생성적 작업에서 사용될 수 있습니다. 본 글에서는 파이토치를 활용하여 GAN과 VAE를 구현하는 방법에 대해 설명하였으며, 각 모델의 동작 원리를 이해하고 실제로 코드로 구현해보는 기회를 제공하였습니다. 생성 모델인 GAN과 VAE는 이미지 생성, 스타일 변환, 데이터 증강 등 다양한 분야에서 활용되고 있습니다. 이러한 모델들은 앞으로 더욱 발전할 가능성이 있으며, 인공지능 분야에서 중요한 역할을 하게 될 것입니다.

파이토치를 활용한 GAN 딥러닝, VAE 훈련

1. 서론

최근 몇 년 간 인공지능 분야에서 생성적 적대 신경망(Generative Adversarial Networks, GAN)과 변분 오토인코더(Variational Autoencoder, VAE)는 데이터 생성 및 변형에서 혁신적인 기술로 자리 잡았습니다. 이들 모델은 서로 다른 방식으로 데이터를 생성하는데, GAN은 두 개의 신경망이 경쟁하는 구조로 이루어져 있고, VAE는 확률적 모델로 데이터를 압축하고 생성하는 방식으로 작동합니다.

2. GAN의 개념과 구조

GAN은 Ian Goodfellow가 2014년에 제안한 모델로, 생성기(Generator)와 판별기(Discriminator)로 구성됩니다. 생성기는 무작위 노이즈를 입력받아 데이터를 생성하고, 판별기는 입력받은 데이터가 진짜인지 가짜인지 판단합니다. 이 두 네트워크는 서로 경쟁하며 학습하게 되며, 이 과정에서 생성기는 점점 더 사실적인 데이터를 생성하게 됩니다.

2.1 GAN 작동 원리

GAN의 훈련 과정은 다음과 같습니다:

  1. 생성기 훈련: 생성기는 랜덤 노이즈 벡터를 입력받아 가짜 이미지를 생성합니다. 생성된 이미지는 판별기의 입력으로 전달됩니다.
  2. 판별기 훈련: 판별기는 진짜 이미지와 가짜 이미지를 받아 각각의 확률을 출력합니다. 판별기의 목표는 가짜 이미지를 올바르게 식별하는 것입니다.
  3. 손실 함수 계산: 생성기와 판별기의 손실 함수가 계산됩니다. 생성기의 목표는 판별기를 속이는 것이고, 판별기는 가짜 이미지를 올바르게 식별하는 것입니다.
  4. 네트워크 업데이트: 손실을 기반으로 네트워크의 가중치가 업데이트됩니다.
  5. 반복: 위의 과정을 반복하며 각 네트워크의 성능이 향상됩니다.

3. VAE의 개념과 구조

변분 오토인코더(VAE)는 오토인코더의 변형으로, 데이터의 분포를 모델링하여 새로운 데이터를 생성하는 능력을 제공합니다. VAE는 인코더(Encoder)와 디코더(Decoder)로 구성되어 있으며, 데이터의 잠재 공간(latent space)을 학습합니다.

3.1 VAE 작동 원리

VAE의 훈련 과정은 다음과 같습니다:

  1. 입력 데이터 인코딩: 인코더는 입력 데이터를 잠재 공간으로 매핑하여 평균과 분산을 생성합니다.
  2. 샘플링: 평균과 분산을 사용하여 잠재 공간에서 샘플링합니다.
  3. 디코딩: 샘플링한 잠재 벡터를 디코더에 입력하여 원래 데이터와 유사한 데이터를 생성합니다.
  4. 손실 함수 계산: VAE는 재구성 손실과 Kullback-Leibler (KL) 발산을 포함하는 손실 함수를 최소화합니다.
  5. 네트워크 업데이트: 손실을 기반으로 가중치가 업데이트됩니다.
  6. 반복: 위의 과정을 반복하며 모델의 품질이 향상됩니다.

4. GAN과 VAE의 차이점

GAN과 VAE는 모두 데이터를 생성하는 모델이지만, 그 접근 방식에 있어 몇 가지 중요한 차이점이 있습니다:

  • 모델 구조: GAN은 생성기와 판별기로 구성되어 경쟁하는 구조를 가지며, VAE는 인코더-디코더 구조로 구성되어 있습니다.
  • 손실 함수: GAN은 두 네트워크의 대립 관계를 통해 학습하며, VAE는 재구성 및 KL 발산을 통해 학습합니다.
  • 데이터 생성 방식: GAN은 현실적인 이미지를 생성하는 데 뛰어난 반면, VAE는 다양성과 연속성이 강조됩니다.

5. 파이토치를 활용한 GAN 구현

이제 파이토치를 활용하여 GAN을 구현해 보겠습니다. MNIST 데이터셋을 통해 손글씨 숫자 이미지를 생성하는 예제를 살펴봅니다.

5.1 라이브러리 설치

pip install torch torchvision matplotlib

5.2 데이터셋 불러오기

import torch
from torchvision import datasets, transforms

transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,))])
train_data = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_data, batch_size=64, shuffle=True)

5.3 GAN 모델 정의

class Generator(torch.nn.Module):
        def __init__(self):
            super(Generator, self).__init__()
            self.model = torch.nn.Sequential(
                torch.nn.Linear(100, 256),
                torch.nn.ReLU(),
                torch.nn.Linear(256, 512),
                torch.nn.ReLU(),
                torch.nn.Linear(512, 1024),
                torch.nn.ReLU(),
                torch.nn.Linear(1024, 784),
                torch.nn.Tanh()
            )

        def forward(self, x):
            return self.model(x).view(-1, 1, 28, 28)

    class Discriminator(torch.nn.Module):
        def __init__(self):
            super(Discriminator, self).__init__()
            self.model = torch.nn.Sequential(
                torch.nn.Flatten(),
                torch.nn.Linear(784, 512),
                torch.nn.LeakyReLU(0.2),
                torch.nn.Linear(512, 256),
                torch.nn.LeakyReLU(0.2),
                torch.nn.Linear(256, 1),
                torch.nn.Sigmoid()
            )

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

5.4 모델 훈련

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
generator = Generator().to(device)
discriminator = Discriminator().to(device)

criterion = torch.nn.BCELoss()
optimizer_G = torch.optim.Adam(generator.parameters(), lr=0.0002, betas=(0.5, 0.999))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=0.0002, betas=(0.5, 0.999))

num_epochs = 200
for epoch in range(num_epochs):
    for i, (images, _) in enumerate(train_loader):
        images = images.to(device)
        batch_size = images.size(0)

        # 진짜 및 가짜 레이블 생성
        real_labels = torch.ones(batch_size, 1).to(device)
        fake_labels = torch.zeros(batch_size, 1).to(device)

        # 판별기 훈련
        optimizer_D.zero_grad()
        outputs = discriminator(images)
        d_loss_real = criterion(outputs, real_labels)
        d_loss_real.backward()

        noise = torch.randn(batch_size, 100).to(device)
        fake_images = generator(noise)
        outputs = discriminator(fake_images.detach())
        d_loss_fake = criterion(outputs, fake_labels)
        d_loss_fake.backward()
        optimizer_D.step()

        # 생성기 훈련
        optimizer_G.zero_grad()
        outputs = discriminator(fake_images)
        g_loss = criterion(outputs, real_labels)
        g_loss.backward()
        optimizer_G.step()

    print(f'Epoch [{epoch}/{num_epochs}], d_loss: {d_loss_real.item() + d_loss_fake.item():.4f}, g_loss: {g_loss.item():.4f}')

6. 파이토치를 활용한 VAE 구현

이제 VAE를 구현해 보겠습니다. 이번에도 MNIST 데이터셋을 사용하여 손글씨 숫자 이미지를 생성하는 예제를 살펴보겠습니다.

6.1 VAE 모델 정의

class VAE(torch.nn.Module):
        def __init__(self):
            super(VAE, self).__init__()
            self.encoder = torch.nn.Sequential(
                torch.nn.Flatten(),
                torch.nn.Linear(784, 400),
                torch.nn.ReLU()
            )

            self.fc_mu = torch.nn.Linear(400, 20)
            self.fc_var = torch.nn.Linear(400, 20)

            self.decoder = torch.nn.Sequential(
                torch.nn.Linear(20, 400),
                torch.nn.ReLU(),
                torch.nn.Linear(400, 784),
                torch.nn.Sigmoid()
            )

        def encode(self, x):
            h = self.encoder(x)
            return self.fc_mu(h), self.fc_var(h)

        def reparameterize(self, mu, logvar):
            std = torch.exp(0.5 * logvar)
            eps = torch.randn_like(std)
            return mu + eps * std

        def decode(self, z):
            return self.decoder(z)

        def forward(self, x):
            mu, logvar = self.encode(x)
            z = self.reparameterize(mu, logvar)
            recon_x = self.decode(z)
            return recon_x, mu, logvar

6.2 VAE 손실 함수

def vae_loss(recon_x, x, mu, logvar):
        BCE = torch.nn.functional.binary_cross_entropy(recon_x, x, reduction='sum')
        return BCE + 0.5 * torch.sum(torch.exp(logvar) + mu.pow(2) - 1 - logvar)

6.3 VAE 모델 훈련

vae = VAE().to(device)
optimizer_VAE = torch.optim.Adam(vae.parameters(), lr=1e-3)

num_epochs = 100
for epoch in range(num_epochs):
    for i, (images, _) in enumerate(train_loader):
        images = images.to(device)

        optimizer_VAE.zero_grad()
        recon_images, mu, logvar = vae(images)
        loss = vae_loss(recon_images, images, mu, logvar)
        loss.backward()
        optimizer_VAE.step()

    print(f'Epoch [{epoch}/{num_epochs}], Loss: {loss.item():.4f}')

7. 결론

본 글에서는 GAN과 VAE의 개념과 구조에 대해 살펴보고, 파이토치를 활용하여 이들 모델을 구현해 보았습니다. GAN은 생성기와 판별기가 경쟁하는 구조를 통해 사실적인 이미지를 생성하는 데 강력한 반면, VAE는 잠재 공간을 통해 데이터를 모델링하고 생성하는 데 우수한 성능을 보입니다. 이 두 모델의 특징을 이해하고 활용하면 다양한 데이터 생성 문제를 해결할 수 있습니다.

8. 참고 문헌