GAN의 G와 D 객체
전체코드 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..