Post

10. WGAN-GP 이론 및 구현

WGAN-GP를 이해하기 위한 수식

1. 유클리드 거리(Euclidean Distance)

유클리드 거리는 두 점 사이의 직선 거리를 측정한다. 2차원 유클리드 공간에서 두 점 $P_1(x_1, y_1)$과 $P_2(x_2, y_2)$ 사이의 유클리드 거리는 다음 공식으로 계산:

$\text{Euclidean distance} = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}$

이 공식은 $n$차원으로 확장될 수 있으며, 이 경우 두 점 $P_1$과 $P_2$ 사이의 거리는 다음과 같이 계산:

$\text{Euclidean distance} = \sqrt{\sum_{i=1}^{n} (p_{2i} - p_{1i})^2}$

여기서 $p_{1i}$ 와 $p_{2i}$ 는 각각 점 $P_1$ 과 $P_2$ 의 $i$ 번째 좌표




2. 유클리드 노름(Euclidean Norm)

기호 $| \cdot |_2$는 2-노름(2-norm) 또는 유클리드 노름(Euclidean norm)을 나타낸다. 이는 벡터의 각 성분의 제곱합의 제곱근으로 계산되며,

벡터의 크기(길이)를 측정하는 데 사용된다. 특히, 그래디언트 패널티(Gradient Penalty, GP)에서 사용될 때,

$|\nabla_{\hat{x}}D(\hat{x})|_2$는 판별자 $D$의 출력에 대한 $\hat{x}$에서의 그래디언트 벡터의 유클리드 노름을 의미한다.



$| \cdot |_2$의 계산

유클리드 노름은 벡터의 크기나 길이를 측정하는 방법으로, 벡터가 원점으로부터 얼마나 떨어져 있는지를 나타낸다. 벡터 $\mathbf{v} = (v_1, v_2, …, v_n)$ 에 대한 유클리드 노름(2-노름)은 다음 공식으로 계산된다:

$|v|_2 = \sqrt{v_1^2 + v_2^2 + … + v_n^2}$

이는 $n$차원 공간에서 벡터 $\mathbf{v}$의 원점으로부터의 직선 거리를 나타낸다. 즉, 각 성분을 제곱하여 합한 후, 그 합의 제곱근을 취하며 이는 벡터의 “길이”를 나타내는 데 사용한다.




3. 그래디언트 패널티에서의 $| \cdot |_2$

WGAN-GP에서 그래디언트 패널티 항:

$GP = \lambda (|\nabla_{\hat{x}}D(\hat{x})|_2 - 1)^2$

여기서 $|\nabla_{\hat{x}}D(\hat{x})|_2$는 판별자 $D$의 출력에 대한 $\hat{x}$에서의 그래디언트 벡터의 유클리드 노름을 의미한다. 그래디언트 패널티는 이 노름이 1에 가까워지도록 강제함으로써, 판별자가 Lipschitz 연속성을 유지하도록 한다. 이는 판별자의 학습 과정에서 그래디언트가 너무 크거나 작아지는 것을 방지하여, GAN의 안정성과 성능을 개선한다.


노름은 벡터가 나타내는 점과 원점 사이의 거리를 나타내며, 이를 통해 벡터의 크기를 비교하거나, 벡터 공간에서의 연산을 정의하는 데 사용된다. 예를 들어, 그래디언트 벡터의 노름을 계산하는 것은 그래디언트가 나타내는 “변화율”의 크기를 측정하는 것으로, 최적화 문제에서 매우 중하다.

  • 1을 빼는 이유는 그래디언트 벡터의 유클리드 노름이 1에서 얼마나 벗어나 있는지를 측정하기 위한 것
  • 그래디언트 노름이 1과 일치하는 경우, 이상적인 형태로 간주되며, 이때 패널티는 0
  • 그래디언트 노름이 1보다 크거나 작은 경우, 즉 기울기가 너무 가파르거나 너무 완만한 경우, 1에서의 차이를 제곱함으로써 얻어진 값은 패널티로 작용해 손실함수에 추가
  • 이 패널티는 Discriminator가 립시츠 조건을 위반하는 것에 대한 벌로 작용해 Discriminator의 학습 과정 조정




4. 유사성

유클리드 거리와 유클리드 노름은 유사한 계산 방식을 공유한다. 둘 다 제곱합의 제곱근을 사용하여 거리나 길이를 계산한다. 실제로, 벡터 $\mathbf{v}$의 유클리드 노름은 벡터 $\mathbf{v}$와 원점 사이의 유클리드 거리로 해석할 수 있다. 즉, 유클리드 노름은 원점과의 유클리드 거리로 볼 수 있으며, 이는 벡터를 하나의 점으로 간주할 때 그 점과 원점 사이의 거리를 측정하는 것과 동일하다.

유클리드 거리와 유클리드 노름은 둘 다 유클리드 공간에서 거리를 측정하는 데 사용되는 중요한 수학적 도구로 거리를 측정하는 대상이 두 점 사이인지(유클리드 거리) 아니면 벡터와 원점 사이인지(유클리드 노름)에 따라 다른 용어를 사용한다.




그래디언트 패널티에서의 역할

WGAN-GP에서 그래디언트 패널티는 판별자의 그래디언트 노름이 1에서 벗어나지 않도록 함으로써, 판별자 함수가 Lipschitz 연속성을 만족하도록 강제한다. 이는 GAN 학습의 안정성을 개선하는 데 핵심적인 역할을 한다. Lipschitz 연속성은 함수의 “급격한 변화”를 제한하는 성질로, 이를 통해 학습 과정이 더욱 안정되고 예측 가능해진다.




Lipschitz 연속성

Lipschitz 연속성은 함수의 변화율이 한정된 범위 내에서 유지되어야 함을 나타내는 수학적 조건. 구체적으로, 함수 $f$ 가 Lipschitz 연속이라고 하려면, 모든 $x_1$ 과 $x_2$ 에 대해 다음 불등식이 성립하는 상수 $L$ 이 존재해야 한다:

$\vert f(x_1) - f(x_2)\vert \leq L\vert x_1 - x_2\vert$

여기서 $L$ 은 Lipschitz 상수로 불리며, 이는 함수가 얼마나 급격하게 변할 수 있는지를 한계 지어준다. $L$ 의 값이 작을수록 함수는 더 부드럽게 변화하며, $L$ 의 값이 클수록 함수는 더 급격한 변화를 보일 수 있다.



함수의 그래디언트가 1 이하로 유지되어야 하는 이유

WGAN-GP에서는 판별자 함수의 Lipschitz 연속성을 강제하기 위해 그래디언트 패널티를 사용한다. 이는 다음과 같은 이유로 중요한데:

  1. 학습의 안정성: GAN에서 판별자가 너무 강력해지면, 생성자가 판별자를 따라잡기 어려워 학습이 불안정해질 수 있다. 판별자의 그래디언트가 1 이하로 제한됨으로써, 판별자의 학습 속도를 적절히 조절하고, 생성자와 판별자 사이의 경쟁을 보다 공정하게 만든다.


  1. Wasserstein 거리의 정확한 추정: WGAN의 목적은 실제 데이터 분포와 생성된 데이터 분포 사이의 Wasserstein 거리를 최소화하는 것이다. 판별자 함수가 Lipschitz 연속성을 만족할 때, 이 거리를 올바르게 추정할 수 있으며, 이는 모델의 성능 향상으로 이어진다.


  1. 모드 붕괴 방지: 판별자의 그래디언트가 제한됨으로써, 생성자가 판별자를 속이기 위해 한정된 몇 가지 패턴에만 의존하는 것이 아니라, 더 다양한 데이터를 생성하도록 유도할 수 있다. 이는 모델이 더 다양한 데이터를 학습하고, 모드 붕괴(mode collapse) 문제를 방지하는 데 도움이 된다.


결론적으로, 판별자 함수의 그래디언트를 1 이하로 유지하는 것은 WGAN-GP의 핵심 요소 중 하나로, 이를 통해 GAN 학습 과정의 안정성을 높이고, 생성된 이미지의 품질을 개선하는 데 기여한다.



그래디언트 패널티의 정의

WGAN-GP (Wasserstein GAN with Gradient Penalty)에서 도입된 그래디언트 패널티(Gradient Penalty, GP)는 판별자의 그래디언트 노름을 제한함으로써 판별자 함수가 1-Lipschitz 조건을 만족하도록 강제한다. 이 조건은 학습 과정을 안정화시키고, 모델의 성능을 개선하는 데 중요한 역할을 한다.

그래디언트 패널티는 실제 이미지와 생성된 이미지 사이의 보간된 샘플들에 대한 판별자의 그래디언트 노름이 1에 가깝도록 제약을 가한다. 그래디언트 패널티 항은 다음과 같이 정의된다:

\[GP = \lambda \mathbb{E}_{\hat{x} \sim \mathbb{P}_{\hat{x}}} [( \vert \nabla_{\hat{x}}D(\hat{x}) \vert _2 - 1)^2]\]

여기서:

  • $\lambda$ 는 패널티의 강도를 조절하는 하이퍼파라미터.
  • \(\mathbb{E}_{\hat{x} \sim \mathbb{P}_{\hat{x}}}\) 는 보간된 샘플, ${\hat{x}}$ 에 대한 기대값. ${\hat{x}}$ 는 실제 이미지와 생성된 이미지 사이를 보간하는 샘플.
  • $\nabla_{\hat{x}}D(\hat{x})$는 판별자 $D$의 출력에 대한 $\hat{x}$에서의 그래디언트(기울기) 벡터.
  • $\vert \cdot \vert _2$ 는 유클리드 노름(2-노름)을 나타내며, 그래디언트 벡터의 길이(크기)를 측정.



유클리드 노름과 그래디언트 패널티

그래디언트 패널티에서 유클리드 노름은 판별자의 그래디언트의 크기를 측정하는 데 사용. 유클리드 노름을 통해 계산된 그래디언트의 크기(길이)는 다음과 같이 표현된다:

\[\vert \nabla_{\hat{x}}D(\hat{x}) \vert _2 = \sqrt{\sum_{i=1}^{n} (\frac{\partial D}{\partial x_i})^2}\]

이 공식에서 $\frac{\partial D}{\partial x_i}$는 판별자 $D$의 출력을 $\hat{x}$의 각 성분 $x_i$ 에 대해 편미분한 값으로 그래디언트 패널티는 이 노름이 가능한 1에 가까워지도록 하는 제약을 추가함으로써, 판별자가 너무 극단적인 값을 출력하지 않고, 모델의 학습이 더 안정적으로 진행되도록 한다.



그래디언트 패널티의 역할

그래디언트 패널티는 판별자가 너무 강력해지는 것을 방지하고, 생성자와 판별자 사이의 경쟁이 공정하게 유지되도록 한다. 이는 GAN 학습에서 발생할 수 있는 모드 붕괴(mode collapse)와 같은 문제를 완화하고, 결과적으로 더 다양하고 고품질의 이미지를 생성할 수 있게 한다. 또한, 그래디언트 패널티를 사용함으로써, WGAN-GP는 원래 WGAN에서 요구되는 가중치 클리핑(weight clipping) 방법의 단점을 극복하고, 학습 과정을 더욱 안정화시켰다.



보간의 예시

보간(Interpolation)은 주어진 데이터 포인트 사이의 값을 추정하는 과정. 보간의 목적은 알려진 데이터 포인트 사이에서 미지의 데이터 포인트의 값을 예측하거나, 데이터를 더 부드럽게 만들기 위해 중간 값을 생성하는 것.

예를 들어, 어떤 함수 $f(x)$의 값이 $x=1$일 때 10, $x=3$일 때 30으로 알려져 있다고 가정해보자. 그러나 $x=2$에서의 함수 값이 주어지지 않았을 때, $x=1$과 $x=3$ 사이의 값을 사용하여 $x=2$ 에서의 값을 추정할 수 있다. 이 경우, 가장 간단한 보간 방법인 선형 보간을 사용하면 $x=2$ 에서의 값이 20이 된다. 이처럼 보간은 알려진 데이터 포인트를 기반으로 중간에 위치한 미지의 데이터 포인트의 값을 추정하는 과정이다.



WGAN-GP에서의 보간

WGAN-GP에서 보간은 실제 이미지와 가짜 이미지 사이의 중간 샘플을 생성하는 데 사용된다. 이 과정은 그래디언트 패널티를 계산하기 위해 필요하다. 실제 샘플과 가짜 샘플 사이의 무작위 보간 포인트를 생성하고, 이 포인트들에 대한 판별자의 그래디언트를 계산하여 판별자가 1-Lipschitz 연속 함수를 만족하도록 한다.

예를 들어, 실제 이미지를 나타내는 텐서 $A$와 가짜 이미지를 나타내는 텐서 $B$가 있을 때, 무작위 가중치 $\alpha$를 사용하여 두 이미지 사이의 보간된 샘플을 생성할 수 있다:

$\text{Interpolated Sample} = \alpha A + (1 - \alpha) B$

여기서 $\alpha$ 는 0과 1 사이의 값으로, 각 샘플에 대한 가중치를 나타낸다. 이렇게 생성된 보간된 샘플에 대한 그래디언트 패널티를 계산함으로써, 판별자의 학습 과정을 안정화시키고, 모델의 성능을 개선할 수 있다.








WGAN-GP (Wasserstein GAN with Gradient Penalty)는 WGAN의 개선된 버전으로, 원래 WGAN에서 가중치 클리핑(weight clipping)을 사용하는 대신 그래디언트 패널티(gradient penalty)를 도입하여 판별자(discriminator)의 Lipschitz 조건을 강제한다. 이 변경은 학습 과정의 안정성을 더욱 향상시키고, 생성된 이미지의 품질을 개선하는 데 도움을 준다.

WGAN-GP의 핵심 개념

  • 그래디언트 패널티 (Gradient Penalty): 판별자의 그래디언트가 1 주변에서만 강한 제약을 받도록 함으로써, Lipschitz 연속성을 유지한다. 이는 가중치 클리핑 방식의 문제점을 해결하고, 판별자의 학습 안정성을 개선한다.


  • 무작위 샘플링을 통한 보간: 실제 이미지와 가짜 이미지 사이의 보간된 샘플을 생성하고, 이러한 샘플에 대한 판별자의 그래디언트 크기를 제한한다. 이 과정은 판별자가 전체 데이터 분포에 대해 Lipschitz 연속성을 유지하도록 돕는다.



WGAN-GP의 손실 함수

WGAN-GP의 손실 함수는 기본적인 WGAN의 손실 함수에 그래디언트 패널티 항을 추가하여 구성된다.

  • 판별자 손실 (Discriminator Loss):
\[L_D = \mathbb{E}_{\tilde{x} \sim \mathbb{P}_g}[D(\tilde{x})] - \mathbb{E}_{x \sim \mathbb{P}_r}[D(x)] + \lambda (\|\nabla_{\hat{x}}D(\hat{x})\|_2 - 1)^2\]
  • 여기서 $\mathbb{E}_{x \sim \mathbb{P}_r}[D(x)]$ 는 실제 이미지에 대한 판별자의 평가의 기대값을,
  • $\mathbb{E}_{\tilde{x} \sim \mathbb{P}_g}[D(\tilde{x})]$ 는 가짜 이미지에 대한 판별자의 평가의 기대값을 나타낸다.
  • $\lambda (|\nabla_{\hat{x}}D(\hat{x})|_2 - 1)^2$는 그래디언트 패널티 항으로, $\lambda$는 패널티의 강도를 조절하는 하이퍼파라미터. $\hat{x}$ 는 실제 이미지와 가짜 이미지 사이를 보간한 샘플이며, 이 항은 판별자의 그래디언트 크기가 1에 가깝게 유지되도록 한다.


  • 생성자 손실 (Generator Loss):
\[L_G = -\mathbb{E}_{\tilde{x} \sim \mathbb{P}_g}[D(\tilde{x})]\]
  • 생성자 손실은 기본적인 WGAN과 동일하게, 판별자가 생성된 이미지에 대해 높은 값을 출력하도록 생성자를 학습시킵니다.

WGAN-GP (Wasserstein GAN with Gradient Penalty)의 판별자 손실 함수는 기본 WGAN 손실에 추가적으로 그래디언트 패널티(Gradient Penalty, GP) 항을 포함한다. 이를 통해 판별자가 Lipschitz 연속성 조건을 만족하도록 강제한다. 그래디언트 패널티 항은 판별자의 함수가 1-Lipschitz 함수를 만족하도록 한다. 이는 함수의 그래디언트(기울기)가 모든 지점에서 1 이하로 유지되어야 함을 의미한다.



그래디언트 패널티 (Gradient Penalty):

그래디언트 패널티 항은 다음과 같이 정의:

$GP = \lambda (|\nabla_{\hat{x}}D(\hat{x})|_2 - 1)^2$

여기서:

  • $\lambda$ 는 패널티의 강도를 조절하는 하이퍼파라미터
  • $\nabla_{\hat{x}}D(\hat{x})$는 판별자 $D$의 출력에 대한 $\hat{x}$에서의 그래디언트(기울기)입니다.
  • $\hat{x}$는 실제 이미지와 가짜 이미지 사이의 보간(interpolation)된 샘플입니다. 즉, $\hat{x} = \epsilon x + (1 - \epsilon)\tilde{x}$, 여기서 $\epsilon$은 0과 1 사이의 무작위 값입니다.

그래디언트 패널티는 판별자의 그래디언트 크기가 1에 가까워지도록 한다. 이는 판별자의 함수가 Lipschitz 연속성을 만족하도록 강제하며, WGAN-GP의 학습 안정성과 성능을 크게 향상시킨다.



결론

WGAN-GP는 가중치 클리핑 대신 그래디언트 패널티를 사용하여 판별자의 Lipschitz 연속성을 보장함으로써, WGAN의 학습 안정성과 생성된 이미지의 질을 개선합니다. 이러한 접근 방식은 GAN 학습에서의 일반적인 문제점을 해결하며, 더욱 신뢰할 수 있는 모델을 구축할 수 있게 한다.








1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import argparse
import os
import numpy as np
import math
import cv2
import matplotlib.pyplot as plt

import torchvision.transforms as transforms
from torchvision.utils import save_image
from torch.utils.data import DataLoader
from torchvision import datasets
from torch.autograd import Variable

import torch.nn as nn
import torch.nn.functional as F
import torch.autograd as autograd
import torch


os.makedirs("images", exist_ok=True)

def weights_init_normal(m):
	classname = m.__class__.__name__
	if classname.find("Conv") != -1:
		torch.nn.init.normal_(m.weight.data, 0.0, 0.02)
	elif classname.find("BatchNorm") != -1:
		torch.nn.init.normal_(m.weight.data, 1.0, 0.02)
		torch.nn.init.constant_(m.bias.data, 0.0)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Option():
    n_epochs = 200 # 학습할 에폭의 수
    batch_size = 64 # 한 번에 학습할 배치의 크기
    lr = 0.0002 # Adam 최적화기의 학습률
    b1 = 0.5 # Adam 최적화기의 첫 번째 모멘텀 감쇠율
    b2 = 0.999 # Adam 최적화기의 두 번째 모멘텀 감쇠율
    n_cpu = 8 # 배치 생성 시 사용할 CPU 스레드 수
    latent_dim = 100 # 잠재 공간의 차원 수
    img_size = 32 # 각 이미지 차원의 크기
    n_critic = 5 # critics의 수
    channels = 1 # 이미지 채널의 수
    sample_interval = 500 # 이미지 샘플링 간격

opt = Option() # Option 클래스의 인스턴스 생성
# CUDA가 사용 가능한 경우 사용하도록 설정
cuda = True if torch.cuda.is_available() else False
# 이미지의 모양을 설정합니다. (채널 수, 이미지 너비, 이미지 높이)의 형태로 설정
img_shape = (opt.channels, opt.img_size, opt.img_size)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()  # nn.Module의 생성자를 호출하여 초기화

        # 블록 함수 정의: 신경망의 레이어를 구성하는 함수
        def block(in_feat, out_feat, normalize=True):
            layers = [nn.Linear(in_feat, out_feat)]  # 선형 변환 레이어
            if normalize:  # 배치 정규화를 적용할지 여부
                layers.append(nn.BatchNorm1d(out_feat, 0.8))  # 배치 정규화 레이어
            layers.append(nn.LeakyReLU(0.2, inplace=True))  # LeakyReLU 활성화 함수
            return layers

        # 모델 구성: 여러 블록을 순차적으로 연결
        self.model = nn.Sequential(
            *block(opt.latent_dim, 128, normalize=False),  # 첫 번째 블록, 배치 정규화 없음
            *block(128, 256),  # 두 번째 블록
            *block(256, 512),  # 세 번째 블록
            *block(512, 1024),  # 네 번째 블록
            
	        """
	        `np.prod`의 역할
			`np.prod` 함수는 NumPy 라이브러리의 함수로, 입력된 배열의 모든 요소의 곱을 계산한다. 
			여기서 `np.prod(img_shape)`는 `img_shape` 튜플에 포함된 모든 차원의 크기를 곱하여, 그 결과값을 반환한다. 
			이를 통해, 여러 차원을 가진 배열의 전체 요소 개수를 하나의 스칼라 값으로 얻을 수 있다.

			예를 들어, 만약 `img_shape`가 이미지의 채널 수, 높이, 너비를 나타내는 `(1, 28, 28)`이라면, 
			`np.prod(img_shape)`는 `1 * 28 * 28 = 784`를 반환한다.
			이는 생성자가 최종적으로 생성해야 하는 각 이미지의 픽셀 수를 나타낸다.
	        """
            nn.Linear(1024, int(np.prod(img_shape))),  # 최종 선형 레이어
            nn.Tanh()  # Tanh 활성화 함수로 출력을 [-1, 1] 범위로 조정
        )

    def forward(self, z):
        img = self.model(z)  # 모델에 잠재 벡터 z를 입력하여 이미지 생성
        """
        `*img_shape`에서 `*`의 역할
		`*` 연산자는 주로 함수 호출 시 인자 리스트의 확장이나, 여러 값을 반환할 때 사용된다.
		이 경우, `img.view(img.shape[0], *img_shape)`에서 `*img_shape`는 `img_shape` 튜플의 각 요소를 
		별도의 인자로 전달하고자 할 때 사용한다.

		`img_shape`가 `(1, 28, 28)`이라면, `*img_shape`는 이를 `1, 28, 28`으로 확장한다. 
		따라서, `img.view(img.shape[0], *img_shape)` 호출은 
		`img.view(img.shape[0], 1, 28, 28)`과 동일한 효과를 가진다. 
		이는 생성된 이미지를 적절한 형태의 텐서로 재구성하기 위해 사용된다.

		결론적으로, `np.prod(img_shape)`는 생성자가 생성할 이미지의 전체 픽셀 수를 결정하는 데 사용되며, 
		`*img_shape`는 생성된 이미지를 원하는 형태의 텐서로 재구성하는 데 사용된다. 
		이러한 방식으로, 모델은 다차원 이미지 데이터를 효과적으로 처리할 수 있다.
        """
        img = img.view(img.shape[0], *img_shape)  # 생성된 이미지를 원래 이미지 형태로 재구성
        return img  # 생성된 이미지 반환



class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()  # nn.Module의 생성자를 호출하여 초기화

        self.model = nn.Sequential(
            nn.Linear(int(np.prod(img_shape)), 512),  # 입력 이미지를 평탄화하고 512차원으로 변환
            nn.LeakyReLU(0.2, inplace=True),  # LeakyReLU 활성화 함수
            nn.Linear(512, 256),  # 512차원에서 256차원으로 차원 축소
            nn.LeakyReLU(0.2, inplace=True),  # LeakyReLU 활성화 함수
            nn.Linear(256, 1),  # 최종 선형 레이어로 하나의 출력 값 생성
        )

    def forward(self, img):
        img_flat = img.view(img.shape[0], -1)  # 입력 이미지를 평탄화
        validity = self.model(img_flat)  # 평탄화된 이미지를 모델에 입력하여 진짜/가짜 여부 판별
        return validity  # 판별 결과 반환
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 그래디언트 패널티에 사용될 가중치 설정
lambda_gp = 10

# 생성자와 판별자 초기화
generator = Generator()
discriminator = Discriminator()

# CUDA가 사용 가능한 경우, 모델을 GPU로 옮긴다.
if cuda:
    generator.cuda()
    discriminator.cuda()

# 데이터 로더 설정
os.makedirs("dataset/mnist", exist_ok=True)
dataloader = torch.utils.data.DataLoader(
    datasets.MNIST(
        "dataset/mnist",
        train=True,
        download=False,  
        transform=transforms.Compose(
            [transforms.Resize(opt.img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]
        ),  
    ),
    batch_size=opt.batch_size,  
    shuffle=True,  
)

# 최적화 함수 설정
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))

# CUDA 사용 여부에 따라 적절한 텐서 타입 설정
Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# 그래디언트 패널티 계산 함수
def compute_gradient_penalty(D, real_samples, fake_samples):
    """WGAN GP의 그래디언트 패널티 손실을 계산"""
    
    # 실제 샘플과 가짜 샘플 사이의 보간에 사용될 무작위 가중치
    """
	`alpha`는 0과 1 사이의 무작위 값으로 구성된 텐서. 
	이 값은 실제 샘플과 가짜 샘플 사이의 보간(interpolation)에 사용. 
	`alpha`의 형태는 실제 샘플의 배치 크기와 동일하게 설정.
    """
    alpha = Tensor(np.random.random((real_samples.size(0), 1, 1, 1)))
	# [배치 크기, 채널, 높이, 너비] (1채널, 3채널 상관없음)
	# 다차원 데이터에 α 값을 적용할 때, α가 각 샘플의 모든 위치에서 동일한 스칼라 값으로 확장되어 적용

    # 실제 샘플과 가짜 샘플 사이의 무작위 보간 샘플 생성
    """
    여기서 실제 샘플(`real_samples`)과 가짜 샘플(`fake_samples`) 사이에 위치한 새로운 샘플들을 생성. 
    이 샘플들은 실제와 가짜 샘플의 선형 조합으로, `alpha`와 `(1 - alpha)`를 가중치로 사용. 
    생성된 `interpolates`는 그래디언트를 계산하기 위해 `.requires_grad_(True)`로 설정.
    """
    interpolates = (alpha * real_samples + ((1 - alpha) * fake_samples)).requires_grad_(True)

	""" 보간된 샘플(`interpolates`)을 판별자 `D`에 입력하여, 해당 샘플들에 대한 판별자의 출력을 얻는다. """
    d_interpolates = D(interpolates)

	""" 
	그래디언트를 계산할 때 사용되는 `grad_outputs` 인자를 위해, 모든 요소가 1인 텐서를 생성. 
	이는 autograd가 그래디언트를 올바르게 계산할 수 있도록 돕는다.
	"""
    fake = Variable(Tensor(real_samples.shape[0], 1).fill_(1.0), requires_grad=False)




    # 보간 샘플에 대한 판별자의 그래디언트 계산
    """
    `autograd.grad` 함수를 사용하여 보간된 샘플에 대한 판별자의 출력(`d_interpolates`)으로부터 
    보간된 샘플(`interpolates`)에 대한 그래디언트를 계산. 
    여기서 `create_graph=True`는 그래디언트 계산 자체에 대한 그래디언트를 나중에 계산할 수 있게 해주며, 
    이는 그래디언트 패널티를 계산할 때 필요.
    """
    """
	`autograd.grad`는 PyTorch에서 제공하는 함수로, 특정 입력에 대한 출력의 그래디언트를 계산. 
	이 함수는 미분 가능한 텐서(즉, 변수)에 대한 미분 값을 자동으로 계산하는 데 사용. 
	
	함수 인자:
	- outputs: 미분을 계산하고자 하는 출력 텐서. GAN에서는 일반적으로 판별자의 출력을 의미
	- inputs: 그래디언트를 계산할 변수. 여기서는 보간된 이미지(interpolates).
	- grad_outputs: 출력 텐서의 그래디언트에 곱해지는 값으로, 일반적으로 텐서의 형태를 가진다. 
	  여기서는 `fake`라는 변수가 사용되는데, 이는 그래디언트 계산에 사용되는 가중치를 의미할 수 있다.
	
	- create_graph: True로 설정할 경우, 그래디언트 계산 그 자체도 미분 가능한 연산으로 만들어져, 
	  이후에 이 그래디언트를 기반으로 추가적인 그래디언트 계산이 가능.
	  
	- retain_graph: 그래디언트 계산 후에도 계산 그래프를 유지할지 여부를 결정. 
	  일반적으로 더 이상의 그래디언트 계산이 필요할 때 True로 설정.
	  
	- only_inputs: True로 설정되면, 입력에 대한 그래디언트만 반환.
	"""
    gradients = autograd.grad(
        outputs=d_interpolates,
        inputs=interpolates,
        grad_outputs=fake,
        create_graph=True,
        retain_graph=True,
        only_inputs=True,
    )[0]



    # 그래디언트의 크기를 계산
    """
    계산된 그래디언트의 유클리드 노름(2-노름)을 구한 후, 이 값이 1과 얼마나 차이나는지를 제곱하여 평균을 취함으로써 
    그래디언트 패널티를 계산합니다. 이 값은 판별자의 손실 함수에 추가되어, 그래디언트가 1을 초과하지 않도록 판별자를 제약.
    """
    gradients = gradients.view(gradients.size(0), -1) # 배치는 유지하고 flatten
    gradient_penalty = ((gradients.norm(2, dim=1) - 1) ** 2).mean()  # 그래디언트 패널티 계산
    return gradient_penalty



그라디언트

그라디언트 계산은 이 손실 함수가 입력 변수에 대해 어떻게 변화하는지를 나타낸다. 다시 말해, 각 입력 변수에 대한 손실 함수의 편미분 값들을 계산하는 것이다.



그라디언트의 의미

  • 방향: 그라디언트는 함수의 가장 가파른 상승 방향을 나타낸다. 이는 다변수 함수에서 최대 증가율을 가지는 방향으로, 손실 함수에서는 가장 가파르게 증가하는 방향을 의미한다. 반대로, 그라디언트의 음수 방향은 함수가 가장 빠르게 감소하는 방향이며, 이를 활용해 최소값을 찾는 최적화 과정에 사용된다.

  • 크기: 그라디언트의 크기는 함수가 해당 지점에서 얼마나 빠르게 변하는지를 나타낸다. 크기가 클수록 변화율이 크다는 것을 의미하며, 최적화 과정에서는 이 크기를 줄여나가는 것을 목표로 한다.



그라디언트 계산의 중요성

머신러닝과 딥러닝에서, 모델의 학습은 손실 함수의 최소값을 찾는 과정이다. 이 최소값을 찾기 위해, 우리는 그라디언트를 계산하여 손실 함수의 현재 위치에서 어떤 방향으로 파라미터를 조정해야 하는지를 결정한다. 이러한 과정을 그라디언트 디센트(Gradient Descent)라고 하며, 파라미터를 조정함으로써 점차적으로 손실 함수의 최소값을 찾아가는 방법이다.


코드에서 outputs=d_interpolatesinputs=interpolates를 사용하는 부분은 interpolates라는 입력(이 경우, 이미지)에 대해 d_interpolates라는 출력값(일반적으로 판별자의 출력)이 어떻게 결정되는지, 즉 이 출력값이 입력 이미지에 대해 어떻게 변화하는지를 분석하는 과정을 의미한다.


여기서 interpolates는 원본 데이터와 생성된 데이터 사이의 점들을 보간한 데이터를 의미하며, 이는 WGAN-GP(Gradient Penalty를 적용한 Wasserstein GAN)에서 실제 데이터 분포와 생성된 데이터 분포 사이의 거리를 측정하는 데 사용된다. d_interpolates는 이 보간된 데이터를 판별자가 평가한 결과.


autograd.grad 함수는 이러한 d_interpolates의 값이 interpolates에 대해 어떻게 변화하는지, 즉 interpolates의 각 요소가 조금씩 변할 때 d_interpolates가 어떻게 변하는지를 나타내는 그라디언트(기울기)를 계산한다. 이 그라디언트는 판별자의 결정 경계 근처에서 실제 데이터와 생성된 데이터 사이의 거리를 측정하는 데 중요한 역할을 한다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# 학습된 배치 수 추적
batches_done = 0
for epoch in range(opt.n_epochs):
    for i, (imgs, _) in enumerate(dataloader):

        # 실제 이미지를 적절한 텐서 타입으로 변환
        real_imgs = Variable(imgs.type(Tensor))

        # 판별자의 그래디언트를 0으로 초기화
        optimizer_D.zero_grad()

        # 생성자 입력으로 사용할 노이즈 벡터 생성
        z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim))))

        # 잡음으로부터 가짜 이미지 배치 생성
        fake_imgs = generator(z)

        # 실제 이미지에 대한 판별자의 판별 결과 계산
        real_validity = discriminator(real_imgs)
        # 가짜 이미지에 대한 판별자의 판별 결과 계산
        fake_validity = discriminator(fake_imgs)
        # 그래디언트 패널티 계산
        gradient_penalty = compute_gradient_penalty(discriminator, real_imgs.data, fake_imgs.data)
        # 판별자의 손실 계산
        d_loss = -torch.mean(real_validity) + torch.mean(fake_validity) + lambda_gp * gradient_penalty

        # 손실에 대한 그래디언트를 역전파하고, 판별자의 파라미터를 업데이트
        d_loss.backward()
        optimizer_D.step()

        # 생성자의 그래디언트를 0으로 초기화
        optimizer_G.zero_grad()

        # n_critic 스텝마다 생성자를 한 번씩 훈련
        if i % opt.n_critic == 0:
            # 가짜 이미지 배치 다시 생성
            fake_imgs = generator(z)
            # 생성자가 판별자를 얼마나 잘 속였는지에 대한 손실 계산
            fake_validity = discriminator(fake_imgs)
            g_loss = -torch.mean(fake_validity)

            # 손실에 대한 그래디언트를 역전파하고, 생성자의 파라미터 업데이트
            g_loss.backward()
            optimizer_G.step()

            # n_critic 스텝마다 배치 처리 수 업데이트
            batches_done += opt.n_critic
            # 지정된 간격마다 로그를 출력하고 이미지 저장
            if batches_done % opt.sample_interval == 0:
                print(
                    "[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]"
                    % (epoch, opt.n_epochs, i, len(dataloader), d_loss.item(), g_loss.item())
                )
                save_image(fake_imgs.data[:25], "images/%d.png" % batches_done, nrow=5, normalize=True)
                plt.figure(figsize = (5,5))
                img1 = cv2.imread("images/%d.png" %batches_done)
                plt.imshow(img1, interpolation='nearest')
                plt.axis('off')
                plt.show()
This post is licensed under CC BY 4.0 by the author.