버츄얼유튜버

GAN의 G와 D 객체

두원공대88학번뚜뚜 2021. 12. 13. 15:40

전체코드 https://github.com/jwgdmkj/jupiterColab/blob/master/GANProduction_1213.ipynb

전처리

#Gan With Pytorch
import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F

from torch.utils.data import DataLoader as DL
from torchvision import datasets, transforms

from torchvision.utils import save_image
import numpy as np
import os, sys
from matplotlib.pyplot import imshow, imsave

MODEL_NAME = 'GAN'
n_noise = 100
batch_size = 100

transform = transforms.Compose([transforms.ToTensor(), 
                                transforms.Normalize(mean = [0.5], std = [0.5])])
#numpy array를 mean값이 0.5, std값이 0.5로 정규화된 tensor로 변환하도록 하는 함수

#위의 transform형태의 함수를 지정했으니, mnist를 받을 때 데이터는 위의 방식으로 전처리됨
mnist = datasets.MNIST(root='../data/', train=True, transform=transform, download=True)
data_loader = DL(dataset=mnist, batch_size=batch_size, 
                         shuffle=True, drop_last=True)

if not os.path.exists('samples'):
    os.makedirs('samples')
class Discriminator(nn.Module):
    def __init__(self, input_size=784, num_classes=1):
        super(Discriminator, self).__init__()
        self.layer = nn.Sequential(
            nn.Linear(input_size, 512),        #input -> 512의 dimension으로 변환
            nn.LeakyReLU(0.2),                 # ReLU는 0보다 작으면 0으로 출력하지만 LeakyReLU는 0.2x로 출력을 한다.
            nn.Linear(512, 256),               # 512->256으로 변환
            nn.LeakyReLU(0.2), 
            nn.Linear(256, num_classes),       # 256 -> 지정한 class 개수 dimension으로 변환
            nn.Sigmoid(),                      # 함수값을 [0, 1]로 제한하고 0과 1만을 구별하는 것(binary classification)에서 효과적이다.
        )
    
    def forward(self, x):
        y = x.view(x.size(0), -1)
        y = self.layer(y)
        return y

Discriminator

super() 함수를 부르면 여기서 만든 클래스는 nn.Module 클래스의 속성들을 가지고 초기화

forward() 함수는 model 객체를 데이터와 함께 호출하면 자동으로 실행

 

data_loaders를 받아오면, 이를 for문을 통해 images, labels로 나눠줄 수 있다

(밑에서, (images, _)로 나눠줌을 알 수 있다)

 

이 images에 해당하는 x를 받아오면, 이를 view를 통해 resize를 시킨다. 이 때, 새로운 사이즈는

(x.size(0), -1)가 된다.

images는 [100, 100]의 torch size를 지니고 있으므로, np array는 (100, )이 된다는 것.

앞서 전처리에서 이미지파일들을 Compose를 통해 totensor, normalize를 시켰는데,

텐서로 변환해주기 + 이미지데이터의 분산의 중심을 원점으로 맞춰주었다.

즉, 이미지 픽셀값 0~255를 totensor로 타입을 변경해 0~1로 바꾼 다음,

다시 normalize를 통해 -1~1사이로 정규화를 하였다.

 

다음으로 이를 layer함수를 통해 Sequential을 시킨다.

들어온 이미지를 sequential을 통해 무언가를 만들어서 그 결과값을 낼 것이다. 그렇다면...

 

class Generator(nn.Module):
    def __init__(self, input_size=100, num_classes=784):
        super(Generator, self).__init__()
        self.layer = nn.Sequential(
            nn.Linear(input_size, 128),       #input -> 128의 dimension으로 변환
            nn.LeakyReLU(0.2), 
            nn.Linear(128, 256),      
            nn.BatchNorm1d(256),              # feaure들이 layer을 지나갈 수록 서로 다른 분포가 생기거나 batch별로 서로 다른 분포가 생기는 것을 방지하기 위하여 batch 별로 normalization해준다.
            nn.LeakyReLU(0.2),
            nn.Linear(256, 512),
            nn.BatchNorm1d(512),
            nn.LeakyReLU(0.2),
            nn.Linear(512, 1024),
            nn.BatchNorm1d(1024),
            nn.LeakyReLU(0.2),
            nn.Linear(1024, num_classes),     
            nn.Tanh()                         # Tanh 함수는 함수값을 [-1, 1]로 제한시켜 값을 saturate 시킨다는 점에서 sigmoid와 비슷하나 zero-centered 모양이다.
        )                                     
        
    def forward(self, x):
        y = self.layer(x)
        y = y.view(x.size(0), 1, 28, 28)
        return y

Generator

 

(1)

밑의 D_x_loss를 통해, 위 위의 Generator(생성기)가 만들어낸 어떠한 '진또배기'를, 실행함수 내의 D_labels(batch * 1의 1로만 이뤄진 tensor) 텐서 array와 비교를 하여, 오차를 계산한다(손실함수참조). 이것이 D_x_loss.

 

(2)

다음으로, 임의의 np(batch_size * n_noise)짜리 z를 하나 생성한다.

이 임의의 noise를 G를 돌린다음에, 다시 D를 한다! (z_outputs = D(G(z)))

이 때, BatchNorm1d가 있는데, 이는 Batch-normalization을 통해 따로 정리하자...

 

(3)

아무튼 이렇게 만들어진 가짜 G(z)를 D에 넣어, criterion을 통해 손실양을 다시금 계산한다.

실제이미지와 1로 이뤄진 np의 손실함수 + 가짜이미지와 0으로 이뤄진 np의 손실함수 = D_loss

D.zero_grad() = 역전파 단계를 실행하기 전에 변화도를 0으로 만든다.

 

실행함수

criterion = nn.BCELoss()                                               # class 가 두개인 경우 효과적인 BCELoss를 사용
D = Discriminator()
G = Generator(n_noise)  

D_opt = torch.optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999))  # SGD에서 효과적인 momentum과 방향설정으로 효율적으로 gradient를 조절하는 기법인 Adam 사용
G_opt = torch.optim.Adam(G.parameters(), lr=0.0002, betas=(0.5, 0.999))

max_epoch = 50
step = 0
n_critic = 1

D_labels = torch.ones(batch_size, 1)                  # batch size만큼 1으로 채워진 tensor 생성
D_fakes = torch.zeros(batch_size, 1)                  # batch size만큼 0으로 채워진 tensor 생성


for epoch in range(max_epoch):
    for idx, (images, _) in enumerate(data_loader):    # data_loader에서 index에 따른 images 값을 받아 for 문 구동
        ###########(1) Discrimination : Training Discriminator
        x = images
        x_outputs = D(x)
        D_x_loss = criterion(x_outputs, D_labels)     # 원래의 image와 D_labels 값의 loss function을 구한다.
		
        ###########(2) Generator: Test Arbitrary Data
        z = torch.randn(batch_size, n_noise)          # 정규 분포 난수값으로 구성된 (batch_size) X (n_noise) 를 생성
        z_outputs = D(G(z))                           # generative 한 모델을 discriminative한 값을 구한다.
        D_z_loss = criterion(z_outputs, D_fakes)      # 구한 값과 fake image의 loss function을 구한다.
        
        D_loss = D_x_loss + D_z_loss                  
        
        ###########(3) Adjust Disriminator's Parameter
        D.zero_grad()                                 # gradients 값들을 backpropagation 과정에서 계속 더해지기 때문에 그 때마다 0으로 만들어주는 것이 학습하기에 편리하다.
        D_loss.backward()                             # loss를 최소화 시키기 위하여 backpropagation이 필요하고 이때의 방법으로 loss.backward()사용했다. 
        D_opt.step()                                  # 학습을 진행할때마다 parameter들을 갱신한다.

		###########(4) Adjust Generator's Parameter
        if step % n_critic == 0:
            # Training Generator
            z = torch.randn(batch_size, n_noise)      # 정규 분포 난수값으로 구성된 (batch_size) X (n_noise) 를 생성 
            z_outputs = D(G(z))                       # generative 한 모델을 discriminative한 값을 구한다.
            G_loss = criterion(z_outputs, D_labels)   # fake image를 분류한 값과 D_labels 값의 loss function을 구한다.

            G.zero_grad()                             # gradients 값들을 backpropagation 과정에서 계속 더해지기 때문에 그 때마다 0으로 만들어주는 것이 학습하기에 편리하다.
            G_loss.backward()                         # loss를 최소화 시키기 위하여 backpropagation이 필요하고 이때의 방법으로 loss.backward()사용했다. 
            G_opt.step()                              # 학습을 진행할때마다 parameter들을 갱신한다.
        
        if step % 500 == 0:
            print('Step: {}, D_Loss: {}, G_Loss: {}'.format(step, D_loss.detach().numpy().flatten(), G_loss.detach().numpy().flatten()))  # cpu에 저장된 data를 가져오기 위해 detach 사용, 출력 값을 정렬하기 위해 flatten 사용
        
        if step % 1000 == 0:
            G.eval()
            img = get_sample_IMAGE(G, n_noise)
            imsave('samples/{}_step{}.jpg'.format(MODEL_NAME, str(step).zfill(3)), img, cmap='gray')  #sample 경로에 저장
            G.train()
        step += 1
G.eval()
imshow(get_sample_IMAGE(G, n_noise), cmap='gray') # 이미지 plot

(4)

위를 보면, step % n_critic == 0이 되면 실행하는 함수가 있다.

즉, 처음만 빼고 실행한다. 앞서 만든 z_output과 D_labels의 손실함수를 구한다.

이런 것을 할 때마다, G_loss.backward()와 G_opt.step()을 통해 역전파를 구하고, parameter을 갱신한다.

즉, 실질적 weight가 변경되는 순간은 D(G)_opt.step()

이때, loss 변수는 전부 np의 torchsize([]).

 

 

다음에 알아볼거

Batch-normalization

zerograd

loss backward..