본문 바로가기

프로젝트/DALL-E 프로젝트

GANs code review - GAN 코드 구현 (CV 프로젝트)

728x90
반응형
 CV 프로젝트 (3) - GANs code review

GAN과 관련된 논문을 3개정도 읽고, 이제 GAN을 실제로 어떻게 구현하는 지에 대한 github code review를 진행했다.

generator와 관련해서 내가 GAN만 다룬 이유는, diffusion model / VAE 모델은 다른 팀원들이 맡았기 때문이다.

https://github.com/goodfeli/adversarial

 

GitHub - goodfeli/adversarial: Code and hyperparameters for the paper "Generative Adversarial Networks"

Code and hyperparameters for the paper "Generative Adversarial Networks" - GitHub - goodfeli/adversarial: Code and hyperparameters for the paper "Generative Adversarial Networks"

github.com

GAN 논문에 기재된 공식 GANs Github이다. 이 곳에 있는 코드들을 간단하게 리뷰해보았다.

 


공식 Github


보는 것처럼, 굉장히 많은 파일들이 저장되어 있다. 여기서 show_samples과 관련된 파일들은 sample들을 확인하기 위한 파일들이므로 따로 찾아보지 않았다.

 

나는 여기서 tfd_pretrain 폴더를 가장 먼저 살펴보았다.

pretrain.yaml 파일과 train.yaml 파일이 저장되어 있다.

해당 파일들에 대한 내용은 뒤에서 살펴보도록 하고, 이제 README를 한번 살펴보자.

논문의 코드와 hyperparameter 등이 포함되어 있다고 이야기하며, GPU 모델에 대한 내용도 담겨져 있다. 그리고 정확하게 재현하지 못하는 경우에 대비한 코멘트도 담겨져 있다.

해당 코드를 설치하고 나면, 이 repository에 담긴 다양한 yaml 파일에서 pylearn2/scripts/train.py를 호출하여 논문에 소개된 dataset에 대한 모델을 훈련하라고 되어 있다. 위에서 본 yaml 파일은 훈련을 위한 파일이라고 할 수 있다.

 


yaml 파일이란?


그렇다면 yaml 파일은 뭘까? 

https://www.inflearn.com/questions/16184/yaml%ED%8C%8C%EC%9D%BC-%EC%9D%B4%EB%9E%80-%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94

 

yaml파일 이란 무엇인가요 - 인프런 | 질문 & 답변

안녕하세요 강사님너무 질문이 많아서 죄송합니다.yaml파일 이라는 단어를 요 근래 많이 듣고 있는데 정확인 무슨 파일인가요검색해 보지도 않고 무조건 질문을 드리는것 같아서 죄송하지만 쉽

www.inflearn.com

해당 질문에 대한 답변을 참고 했으며, 간단하게 말하자면 다음과 같다.

  • 데이터 규칙이 다른 타 시스템들과 연동할 때, 데이터 포맷에 대한 파일이다.
  • xml, json 파일이 yaml 파일과 유사하게 데이터 포맷을 저장해둔 파일들이다.
  • 가독성 측면에서 xml, json보다 훨씬 뛰어난 파일로 일반적으로 yaml을 권장한다.

앞서, causaLM 프로젝트를 진행할 때는 json 파일을 많이 사용했었는데 이번엔 yaml 파일을 사용해볼 수 있었다.

yaml 파일도 json 파일과 비슷한 역할이지만, 가독성 측면에서 더 뛰어나다는 것을 강조하며 최근 대중적으로 많이 사용되고 있다고 한다. 

지난 프로젝트 때, json 파일을 한번 보긴 했었는데 이번 yaml 파일을 보면서 '아, 정말 가독성은 좋구나!' 를 느끼게 되었다.

 


train.yaml


tfd_pretrain 폴더 안에 있는 2개의 yaml 파일에 대해서 살펴볼텐데, 가장 먼저 train.yaml 파일을 살펴보자.

!obj:pylearn2.train.Train { # pylearn2에서 사용되는 학습 설정을 알려줌.
    dataset: &train !obj:pylearn2.datasets.tfd.TFD { 
        which_set: 'unlabeled', # dataset은 unlabeled 데이터를 사용함.
        scale: True, # normalization (정규화) 진행
    },
    model: !obj:adversarial.AdversaryPair { # model은 adversarial.AdversaryPair 사용
        generator: !obj:adversarial.Generator { # generator 설정 (1번 모델)
            monitor_ll: 1, # log likelihood를 모니터
            mlp: !obj:adversarial.add_layers { # 기존 pretrain 모델에 layer 추가
                mlp: !obj:pylearn2.models.mlp.MLP {
                    layers: [
                     !obj:pylearn2.models.mlp.RectifiedLinear {
                         layer_name: 'h0',
                         dim: 8000,
                         irange: .05,
                         max_col_norm: 1.9365,
                     },
                     !obj:pylearn2.models.mlp.Sigmoid {
                         layer_name: 'h1',
                         dim: 100,
                         irange: .05,
                         max_col_norm: 1.9365,
                         init_bias: -2.0,
                     },
                    ],
                nvis: 100,
                },
                pretrained: "./pretrain.pkl", # 사전 학습된 모델을 불러옴
            }
        },
        discriminator: # discriminator 설정 (2번 모델) ==> 총 2개의 모델 설정
            !obj:pylearn2.models.mlp.MLP {
            layers: [
                     !obj:pylearn2.models.maxout.Maxout {
                         #W_lr_scale: .1,
                         #b_lr_scale: .1,
                         layer_name: 'h0',
                         num_units: 1200,
                         num_pieces: 5,
                         irange: .005,
                         max_col_norm: 1.9365,
                     },
                     !obj:pylearn2.models.maxout.Maxout {
                         #W_lr_scale: .1,
                         #b_lr_scale: .1,
                         layer_name: 'h1',
                         num_units: 1200,
                         num_pieces: 5,
                         irange: .005,
                         max_col_norm: 1.9365,
                     },
                     !obj:pylearn2.models.mlp.Sigmoid {
                         #W_lr_scale: .1,
                         #b_lr_scale: .1,
                         max_col_norm: 1.9365,
                         layer_name: 'y',
                         dim: 1,
                         irange: .005
                     }
                    ],
            nvis: 2304,
        },
    },

# 알고리즘 정의 ==> SGD 알고리즘을 사용하며 각종 hyperparameter 설정
    algorithm: !obj:pylearn2.training_algorithms.sgd.SGD {
        batch_size: 100,
        learning_rate: .05,
        learning_rule: !obj:pylearn2.training_algorithms.learning_rule.Momentum {
            init_momentum: .5,
        },
    monitoring_dataset: # 학습 중에 모니터링 할 데이터 (validation dataset)
           {
    #           'train' : *train,
               'valid' : !obj:pylearn2.datasets.tfd.TFD {
                             which_set: 'valid', 
                             scale: True,
                         },
    #           'test'  : !obj:pylearn2.datasets.tfd.TFD {
    #                         which_set: 'test',
    #                         scale: True,
    #                     }
           },

# cost function 정의 ==> adversarial.AdversaryCost2 함수를 사용한다.
        cost: !obj:adversarial.AdversaryCost2 {
            scale_grads: 0,
            #target_scale: 1.,
            discriminator_default_input_include_prob: .5,
            discriminator_input_include_probs: {
                'h0': .8
            },
            discriminator_default_input_scale: 2.,
            discriminator_input_scales: {
                'h0': 1.25
            }
            },
        #!obj:pylearn2.costs.mlp.dropout.Dropout {
        #    input_include_probs: { 'h0' : .8 },
        #    input_scales: { 'h0': 1. }
        #},
        termination_criterion: !obj:pylearn2.termination_criteria.EpochCounter {
            max_epochs: 22
        },
        update_callbacks: !obj:pylearn2.training_algorithms.sgd.ExponentialDecay {
            decay_factor: 1.000004,
            min_lr: .000001
        }
    },

# 학습 중에 momentum 값을 조정
    extensions: [
        !obj:pylearn2.training_algorithms.learning_rule.MomentumAdjustor {
            start: 1,
            saturate: 250,
            final_momentum: .7
        },
        #!obj:pylearn2.train_extensions.best_params.MonitorBasedSaveBest {
        #    channel_name: 'valid_gen_ll',
        #    name_base: 'save/train',
        #    store_best_model: True
        #}

    ],
    save_path: "${PYLEARN2_TRAIN_FILE_FULL_STEM}.pkl",
    save_freq: 1
}

보면서 느낀 점은, pseudo code와 비슷한 느낌인 것 같았다.

코드 안에 주석으로 설명을 달아두긴 했지만, 간단하게 살펴보면 pylearn2을 사용하여 training을 진행할 때 어떤 학습 설정을 해야 하는 지를 알려주었다. model명과 model 안의 generator와 discriminator의 layer, mlp, activation function 등의 구조를 알려줬으며 알고리즘과 cost function을 정의하는 부분도 있었다.

이게 어떤 의미인지만 파악하면 되는데, 그 이유는 yaml 파일을 가지고 그대로 모델을 구현할 수 있기 때문이다.

 


pretrain.yaml


!obj:pylearn2.train.Train {
    dataset: &train !obj:pylearn2.datasets.tfd.TFD {
        which_set: 'unlabeled',
        scale: True,
    },
    model: !obj:adversarial.AdversaryPair {
        generator: !obj:adversarial.Generator {
            monitor_ll: 1,
            mlp: !obj:pylearn2.models.mlp.MLP {
            layers: [
                     !obj:pylearn2.models.mlp.RectifiedLinear {
                         layer_name: 'h0',
                         dim: 8000,
                         irange: .05,
                         max_col_norm: 1.9365,
                     },
                     !obj:pylearn2.models.mlp.Sigmoid {
                         layer_name: 'h1',
                         dim: 8000,
                         irange: .05,
                         max_col_norm: 1.9365,
                         init_bias: -2.0,
                     },
                     !obj:pylearn2.models.mlp.Sigmoid {
                         max_col_norm: 1.9365,
                         init_bias: !obj:pylearn2.models.dbm.init_sigmoid_bias_from_marginals { dataset: *train},
                         layer_name: 'y',
                         sparse_init: 100,
                         dim: 2304
                     }
                    ],
            nvis: 100,
        }},
        discriminator:
            !obj:pylearn2.models.mlp.MLP {
            layers: [
                     !obj:pylearn2.models.maxout.Maxout {
                         #W_lr_scale: .1,
                         #b_lr_scale: .1,
                         layer_name: 'h0',
                         num_units: 1200,
                         num_pieces: 5,
                         irange: .005,
                         max_col_norm: 1.9365,
                     },
                     !obj:pylearn2.models.maxout.Maxout {
                         #W_lr_scale: .1,
                         #b_lr_scale: .1,
                         layer_name: 'h1',
                         num_units: 1200,
                         num_pieces: 5,
                         irange: .005,
                         max_col_norm: 1.9365,
                     },
                     !obj:pylearn2.models.mlp.Sigmoid {
                         #W_lr_scale: .1,
                         #b_lr_scale: .1,
                         max_col_norm: 1.9365,
                         layer_name: 'y',
                         dim: 1,
                         irange: .005
                     }
                    ],
            nvis: 2304,
        },
    },
    algorithm: !obj:pylearn2.training_algorithms.sgd.SGD {
        batch_size: 100,
        learning_rate: .05,
        learning_rule: !obj:pylearn2.training_algorithms.learning_rule.Momentum {
            init_momentum: .5,
        },
    monitoring_dataset:
           {
    #           'train' : *train,
               'valid' : !obj:pylearn2.datasets.tfd.TFD {
                             which_set: 'valid',
                             scale: True,
                         },
    #           'test'  : !obj:pylearn2.datasets.tfd.TFD {
    #                         which_set: 'test',
    #                         scale: True,
    #                     }
           },
        cost: !obj:adversarial.AdversaryCost2 {
            scale_grads: 0,
            #target_scale: 1.,
            discriminator_default_input_include_prob: .5,
            discriminator_input_include_probs: {
                'h0': .8
            },
            discriminator_default_input_scale: 2.,
            discriminator_input_scales: {
                'h0': 1.25
            }
            },
        #!obj:pylearn2.costs.mlp.dropout.Dropout {
        #    input_include_probs: { 'h0' : .8 },
        #    input_scales: { 'h0': 1. }
        #},
        termination_criterion: !obj:pylearn2.termination_criteria.EpochCounter {
            max_epochs: 50
        },
        update_callbacks: !obj:pylearn2.training_algorithms.sgd.ExponentialDecay {
            decay_factor: 1.000004,
            min_lr: .000001
        }
    },
    extensions: [
        !obj:pylearn2.training_algorithms.learning_rule.MomentumAdjustor {
            start: 1,
            saturate: 250,
            final_momentum: .7
        },
        #!obj:pylearn2.train_extensions.best_params.MonitorBasedSaveBest {
        #    channel_name: 'valid_gen_ll',
        #    name_base: 'save/pretrain',
        #    store_best_model: True
        #}
    ],
    save_path: "${PYLEARN2_TRAIN_FILE_FULL_STEM}.pkl",
    save_freq: 1
}

위의 train.yaml 파일과 비교해보면 크게 다른 점은 없다. 전반적으로 어떤 식인지 파악할 수 있을 정도로 꽤나 깔끔하고 가독성이 좋아서 이해하기 편했다.

 


2개의 yaml 파일의 차이는?


차이는 크게 2개의 부분에서 나타났다.

!obj:pylearn2.models.mlp.Sigmoid {
                         layer_name: 'h1',
                         dim: 8000,
                         irange: .05,
                         max_col_norm: 1.9365,
                         init_bias: -2.0,
                     },

# generator의 h1의 dimension이 8000이지만 train은 100으로 작게 설정되어 있다.

termination_criterion: !obj:pylearn2.termination_criteria.EpochCounter {
            max_epochs: 50

# 학습을 위한 최대 epoch의 수가 50인 반면, train은 22로 작게 설정되어 있다.

pretrain의 경우, 더 큰 모델을 학습시키려고 하다보니 train에 비해 h1의 dimension이 8000이나 되었다.

또한, 학습 epoch의 수도 2배 이상 차이가 났다.

 


Code 구현에 앞서서..


 

yaml 파일을 가지고 README에 나온 방법대로 구현을 해보고 싶었는데, 가장 큰 문제가 발생했다.

바로 pylearn2가 실행이 되지 않는다는 점.

pylearn2의 공식 github에 들어가면, 이제 더 이상 update되지 않는다는 comment가 적혀 있고, 실제로 5년 이상 업데이트된 흔적이 보이지 않았다.

그래서인지는 모르겠으나 git clone이 불가능하여 다운 받는 것 조차 실패했다

 

아쉬운 대로, GAN을 비슷하게 흉내낼 수 있는 코드들을 찾아보기 시작했다.

 

GAN[Generative Adversarial Network] 구현하기 w/ Pytorch

2014년에 발표된 GAN은 이미 너무나 많은 갈래가 생겼습니다. 한 갈래인 CycleGAN에 대해 알게 되었는데, 정말 신기하다는 생각이 들었습니다. 그래서 이왕 궁금한 거, GAN부터 구현해보는 생각을 하

ainote.tistory.com

이 분의 코드를 참고하여 그대로 재현해보기로 했고, 아래 내용들은 이 분의 코드를 참고했음을 밝힌다.

 

 


code 구현


우선 필요한 package들을 import해주자.

import torch
import torch.nn as nn

import numpy as np

# for MNIST data
import torchvision
from torchvision import datasets
import torchvision.transforms as transforms
from torchvision.utils import save_image
from torch.autograd import Variable


import matplotlib.pyplot as plt

 

 

Dataset 준비

batch_size = 64

transforms_train = transforms.Compose([
    transforms.Resize(28),
    transforms.ToTensor(), # data를 pytorch의 tensor 형태로 변환
    transforms.Normalize([0.5], [0.5]) # 픽셀값을 0 ~ 1에서 -1 ~ 1로 변환
])

# train=True => 학습 데이터를 불러오기
train_dataset = datasets.MNIST(root="./dataset", train=True, download=True, transform=transforms_train)

# data를 batch size만큼만 가져오는 dataloader 생성
# shuffle을 허용함으로써 데이터를 무작위로 섞고, num_workers를 통해 병렬 작업의 수를 지정한다.
dataloader = torch.utils.data.DataLoader(train_dataset, batch_size = batch_size, shuffle=True, num_workers=4)

images, labels = next(iter(dataloader)) # iter로 dataloader를 반복, next로 하나씩 꺼내기
img = torchvision.utils.make_grid(images) # grid 형태로 결합하여 여러 이미지를 한번에 볼수 있음.
    • batch size를 64로 설정하여 한번에 64개의 이미지와 label을 처리한다.
    • torchvision의 transforms은 이미지를 정규화 해주는 모듈이다.
      • 이미지를 정규화하는 이유는 backpropagation시, gradient 계산에서 데이터가 유사한 범위를 가지도록 하기 위함이다.
      • 즉, gradient가 최적점을 잘 찾기 위해선 normalization을 진행한, 데이터 분포가 깔끔한 input이 필요하다.
      • 기존 GAN 논문의 github에 저장된 yaml 파일에서 normalization을 하는 부분이 있으므로 이를 반영
      • transforms.Normalize([0.5],[0.5]) & **transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))**의 차이점
        • 전자는 단일 채널 이미지 (흑백 이미지)의 경우에 주로 사용된다. 입력 데이터의 평균 및 표준 편차를 각각 0.5로 설정한다
        • 후자는 다중 채널 이미지 (컬러 이미지)의 경우에 주로 사용된다. 각 채널 (RGB 혹은 다른 채널)의 평균 및 표준편차를 각각 0.5로 설정한다.
      • 일반적으로 image가 tensor 형태로 변할 때, 픽셀 값들은 0~255 에서 0~1로 변환된다. 이를 다시 -1~1의 값으로 normalized 시켜서 이미지를 동일한 환경으로 어느 정도 맞춰 학습을 진행하기 위해 사용한다.

자세한 코드 설명은 주석 처리를 해두었으니 참고하면 좋겠다.

  • sample 하나를 출력하면 다음과 같다.

Generator 구현

# Z벡터를 위한 latent dimension 정의 ==> noise vector
latent_dim = 100

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()

        def block(input_dim, output_dim, normalize=True):
            layers = [nn.Linear(input_dim, output_dim)]
            if normalize:
                layers.append(nn.BatchNorm1d(output_dim, 0.8))
            layers.append(nn.LeakyReLU(0.2, inplace=True))
            return layers

        # generater의 model은 여러개의 block을 쌓아서 만들어짐
        self.model = nn.Sequential(
            *block(latent_dim, 128, normalize=False),
            *block(128, 256),
            *block(256, 512),
            *block(512, 1024),
            nn.Linear(1024, int(np.prod(img_shape))),
            nn.Tanh() 
        )

    def forward(self, z): 
        # z : input noise vector 
        img = self.model(z)
        img = img.view(img.size(0), *img_shape)
        return img
  • latent space를 정의해줬는데, 이는 GAN 내에서 z 벡터의 값을 이미지 feature에 mapping하도록 만든다.
    • dimension은 최소한 나타내려고 하는 대상의 정보를 충분히 담을 수 있을 정도로 커야 한다.
  • nn.module로 받고, block이라는 함수를 지정하여 nn.linear layer를 쌓아준다.
    • 100 → 128 → 256 → 512 → 1024 → 28 x 28 image
    • nn.Sequential을 통해 layer를 가독성 있게 정의.
    • LeakyReLU 함수를 사용
  • tanh 활성화 함수를 사용하여 픽셀 값을 -1~1로 조정

Discriminator 구현

class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()

        self.model = nn.Sequential(
            nn.Linear(int(np.prod(img_shape)), 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):
				# image를 1차원 벡터로 전
        img_flat = img.view(img.size(0), -1)
        validity = self.model(img_flat)

        return validity
  • img_shape → 512 → 256 → 1
    • 최종 결과는 sigmoid 함수를 태워서 판별 결과가 나오게 된다.
    • 0과 1 사이의 숫자 하나로 출력된다.

Loss function & Optimizer

# learning rate 설
lr = 0.0002

# decay of first order momentum of gradient
b1 = 0.5
b2 = 0.999

# Initialize generator and discriminator
generator = Generator()
discriminator = Discriminator()

# Loss function
adversarial_loss = nn.BCELoss()

# Adam Optimizer
optimizer_G = torch.optim.Adam(generator.parameters(), lr=lr, betas=(b1, b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=lr, betas=(b1, b2))

# GPU
cuda = True if torch.cuda.is_available() else False

if cuda : 
  generator.cuda()
  discriminator.cuda()
  adversarial_loss.cuda()
  • b1, b2는 Adam optimizer에서 사용되는 betas의 값들이다.
    • b1은 first moment의 감소율을 의미한다.
      • 기본 값은 0.9이다.
      • 1에 가까울수록 이전 gradient 정보를 많이 반영하고 0에 가까울수록 덜 반영한다.
      • 이전 gradient의 영향을 빠르게 감쇄시키는 역할을 한다.
    • b2는 second moment의 감소율을 의미한다.
      • 기본 값은 0.999이다.
  • image가 진짜일 확률, 그리고 이 확률이 얼마나 정답과 가까운지 측정하기 위해서 binary cross entropy(BCELoss)를 사용한다.
    • 출력한 확률값이 정답에 가까울 경우 낮아지고, 정답에서 멀면 높아진다. loss function의 값을 낮추는 것이 이 학습의 목적이 된다.
  • optimizer로는 Adam을 사용했다.

여기서, b1, b2와 관련하여 gradient의 영향을 빠르게 감쇄 시킬 때, 어떤 이득이 있는 지 살펴보자.

  1. 현재 gradient를 많이 고려하므로 모델이 빠르게 수렴하고, 이는 학습 속도를 향상시킨다.
  2. 모델 파라미터의 업데이트가 불안정한 경우를 방지하여 학습 과정에서 안정성을 확보할 수 있다.
  3. 이전 gradient 정보를 저장하는 메모리 요구 사항이 감소하므로 모델 학습 중, 메모리를 효율적으로 사용할 수 있다.

그렇다면 단점은?

  1. local minimum에 빠질 가능성이 있다.
  2. 빠른 수렴에 의해 overfitting이 될 수 있다.

Training

import time

# epoch 지정
n_epochs = 200 

# image sample간의 거리
sample_interval = 2000 

start_time = time.time()

Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor

for epoch in range(n_epochs):
    for i, (imgs, _) in enumerate(dataloader):

        # Adversarial ground truths
        ## 실제 이미지는 1로, 가짜 이미지는 0으로 labeling
        real = Variable(Tensor(imgs.size(0), 1).fill_(1.0), requires_grad=False)
        fake = Variable(Tensor(imgs.size(0), 1).fill_(0.0), requires_grad=False)

        # Configure input
        real_imgs = Variable(imgs.type(Tensor)) # Variable()은 현재는 torch.Tensor로 대체

        # -----------------
        #  Train Generator
        # -----------------

        optimizer_G.zero_grad()

        # generator의 input은 noise이므로, 무작위 값을 생성
        z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], latent_dim))))

        ## random sampling한 값인 z를 input으로 하여 이미지를 생성
        generated_imgs = generator(z)

        ## 생성된 이미지를 discriminator가 판별하게 한 후, loss값을 계산
        g_loss = adversarial_loss(discriminator(generated_imgs), real)

        # generator update
        g_loss.backward()
        optimizer_G.step()

        # ---------------------
        #  Train Discriminator
        # ---------------------

        optimizer_D.zero_grad()

        ## 실제 이미지는 real(1)로, 가짜 이미지는 fake(0)으로 판별하도록 계산
        real_loss = adversarial_loss(discriminator(real_imgs), real)
        fake_loss = adversarial_loss(discriminator(generated_imgs.detach()), fake)
        d_loss = (real_loss + fake_loss) / 2

        # discriminator 업데이트
        d_loss.backward()
        optimizer_D.step()

        done = epoch * len(dataloader) + i
        if done % sample_interval == 0:
            # 생성된 이미지 중에서 25개만 선택하여 5 X 5 격자 이미지에 출력
            save_image(generated_imgs.data[:25], f"data{epoch}.png", nrow=5, normalize=True)

    # 하나의 epoch이 끝날 때마다 로그(log) 출력
    print(f"[Epoch {epoch}/{n_epochs}] [D loss: {d_loss.item():.6f}] [G loss: {g_loss.item():.6f}] [Elapsed time: {time.time() - start_time:.2f}s]")

 

 


Experiment 및 결과


training을 진행하면 다음과 같이 epoch가 진행되는 것을 확인할 수 있다. 그 후, 50 epoch마다 사진을 저장하여 이를 시각화 하기 위해 matplotlib을 사용했다.

import matplotlib.image as mpimg

epoch0 = mpimg.imread('/content/data0.png')
epoch51 = mpimg.imread('/content/data51.png')
epoch100 = mpimg.imread('/content/data100.png')
epoch151 = mpimg.imread('/content/data151.png')
epoch198 = mpimg.imread('/content/data198.png')

plt.figure(figsize=(20,10))
plt.subplot(2,3,1)
plt.title('epoch-0')
plt.imshow(epoch0)

plt.subplot(2,3,2)
plt.title('epoch-51')
plt.imshow(epoch51)

plt.subplot(2,3,3)
plt.title('epoch-100')
plt.imshow(epoch100)

plt.subplot(2,3,4)
plt.title('epoch-151')
plt.imshow(epoch151)

plt.subplot(2,3,5)
plt.title('epoch-198')
plt.imshow(epoch198)

epoch가 진행됨에 따라 data가 어떻게 생성되는 지 확인할 수 있다. 

다만 조금 아쉬운 부분은, 1,7,9와 같은 숫자들이 많이 생성되었으며 그 중에서도 1의 비중이 큰 것으로 보아 좋은 결과는 아닌 것 같다. 

조금 더 자세하게 재현할 수 있었으면 좋았을텐데, 라는 아쉬움이 남았다.


느낀 점


비록 다른 분의 코드를 참고하긴 했지만, 이야기만 정말 많이 들어본 GAN 모델을 비슷하게 구현해볼 수 있어서 좋았다. 특히, 이렇게 논문을 읽고 github에 있는 코드들을 보면서 실제로 어떻게 구현되는 지를 하나씩 살펴보는 재미는 생각보다 엄청나다. 

가끔은 논문으로는 이해되지 않는 내용들도 코드로 이해되는 경우가 있기 때문에, 이런 리뷰와 스터디를 꾸준히 진행하다 보면 나도 언젠가 나 스스로 이런 방법들을 고안해낼 수 있지 않을까, 라는 생각을 하게 된다.

728x90
반응형