commit fca4865a7a52902ba5c00ae517bae529cf18442f Author: pinb Date: Wed Nov 22 03:40:21 2023 +0900 first commit diff --git a/README.md b/README.md new file mode 100644 index 000000000..0a665d7bc --- /dev/null +++ b/README.md @@ -0,0 +1,653 @@ +--- +layout: default +title: 17. GAN +subtitle: Deep Learning +--- +----- + +[PINBlog Gitea Repository](https://gitea.pinblog.codes/CBNU/17_GAN) + +----- + +# GAN +- 산업인공지능학과 대학원 + 2022254026 + 김홍열 + + +--- + + +# **적대적 생성 신경망(Generative Adversarial Networks)이란?** + +GAN이란 학습 데이터들의 분포를 학습해, 같은 분포에서 새로운 데이터를 생성할 수 있도록 DL 모델을 학습시키는 프레임워크다. + +2014년 Ian Goodfellow가 개발했으며, Generative Adversarial Nets 논문에서 처음 소개되었다. + +GAN은 생성자 와 구분자 로 구별되는 두가지 모델을 가지고 있는것이 특징이다. + +생성자의 역할은 실제 이미지로 착각되도록 정교한 이미지를 만드는 것이고, + +구분자의 역할은 이미지를 보고 생성자에 의해 만들어진 이미지인지 실제 이미지인지 알아내는 것이다. + +모델을 학습하는 동안, 생성자는 더 진짜같은 가짜 이미지를 만들어내며 구분자를 속이려 하고, + +구분자는 더 정확히 가짜/진짜 이미지를 구별할 수 있도록 노력한다. + +이 ‘경찰과 도둑’ 게임은, 생성자가 학습 데이터들에서 직접 가져온 것처럼 보일정도로 완벽한 이미지를 만들어내고, + +구분자가 생성자에서 나온 이미지를 50%의 확률로 가짜 혹은 진짜로 판별할 때, 균형상태에 도달하게 된다. + + + + +# **DCGAN이란?** + + +DCGAN은 위에서 기술한 GAN에서 직접적으로 파생된 모델로, + +생성자와 구분자에서 합성곱 신경망(convolution)과 전치 합성곱 신경망(convolution-transpose)을 사용했다는 것이 차이점이다 + +Radford와 그 외가 저술한 Unsupervised Representation Learning With Deep Convolutional Generative Adversarial Networks 논문에서 처음 모델이 소개되었고, + +지금은 대부분의 GAN모델이 DCGAN을 기반으로 만들어지는 중이다. + +이전 GAN과 모델의 구조가 실제로 어떻게 다른지 확인을 해보자면, 먼저 구분자에서는 convolution 계층, batch norm 계층, 그리고 LeakyReLU 활성함수가 사용되었다. + +클래식한 GAN과 마찬가지로, 구분자의 입력 데이터는 3x64x64 의 이미지이고, 출력값은 입력 데이터가 실제 데이터일 0~1사이의 확률값이다. + +다음으로, 생성자는 convolutional-transpose 계층, 배치 정규화(batch norm) 계층, 그리고 ReLU 활성함수가 사용되었다. + +입력값은 역시나 정규분포에서 추출한 잠재공간 벡터 z이고, 출력값은 3x64x64 RGB 이미지이다. + +이때, 전치 합성곱 신경망은 잠재공간 벡터로 하여금 이미지와 같은 차원을 갖도록 변환시켜주는 역할을 한다. + + +--- + +### 예제 코드[¶]() + +
+Import +
+ +```python + +from __future__ import print_function +#%matplotlib inline +import argparse +import os +import random +import torch +import torch.nn as nn +import torch.nn.parallel +import torch.backends.cudnn as cudnn +import torch.optim as optim +import torch.utils.data +import torchvision.datasets as dset +import torchvision.transforms as transforms +import torchvision.utils as vutils +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from IPython.display import HTML + +# 코드 실행결과의 동일성을 위해 무작위 시드를 설정합니다 +manualSeed = 999 +#manualSeed = random.randint(1, 10000) # 만일 새로운 결과를 원한다면 주석을 없애면 됩니다 +print("Random Seed: ", manualSeed) +random.seed(manualSeed) +torch.manual_seed(manualSeed) + + +``` + +
+
+ +
+Hyper Parameters +
+ +```python + +# 데이터셋의 경로 +dataroot = "./dataset/img_align_celeba_png" + +# dataloader에서 사용할 쓰레드 수 +workers = 2 + +# 배치 크기 +batch_size = 128 + +# 이미지의 크기입니다. 모든 이미지를 변환하여 64로 크기가 통일됩니다. +image_size = 64 + +# 이미지의 채널 수로, RGB 이미지이기 때문에 3으로 설정합니다. +nc = 3 + +# 잠재공간 벡터의 크기 (예. 생성자의 입력값 크기) +nz = 100 + +# 생성자를 통과하는 특징 데이터들의 채널 크기 +ngf = 64 + +# 구분자를 통과하는 특징 데이터들의 채널 크기 +ndf = 64 + +# 학습할 에폭 수 +num_epochs = 5 + +# 옵티마이저의 학습률 +lr = 0.0002 + +# Adam 옵티마이저의 beta1 하이퍼파라미터 +beta1 = 0.5 + +# 사용가능한 gpu 번호. CPU를 사용해야 하는경우 0으로 설정하세요 +ngpu = 1 + + +``` + +
+
+ + +--- + +# **데이터셋(Celeb-A Faces dataset)** + +본 튜토리얼에서 사용할 데이터는 [Celeb-A Faces dataset](http://mmlab.ie.cuhk.edu.hk/projects/CelebA.html) 로, + +해당 링크를 이용하거나 [Google Drive](https://drive.google.com/drive/folders/0B7EVK8r0v71pTUZsaXdaSnZBZzg) 에서 데이터를 받을 수 있다. + +데이터를 받으면 img_align_celeba.zip 라는 파일이 있다. + +다운로드가 끝나면 celeba 이라는 폴더를 새로 만들고, 해당 폴더에 해당 zip 파일을 압축해제 해주시면 된다. + +압축 해제 후, 위에서 정의한 dataroot 변수에 방금 만든 celeba 폴더의 경로를 넣는다. + +위의 작업이 끝나면 celeba 폴더의 구조는 다음과 같아야 한다. + +``` + +./dataset/img_align_celeba_png + -> img_align_celeba + -> 188242.jpg + -> 173822.jpg + -> 284702.jpg + -> 537394.jpg + ... + + +``` + + +### 예제 코드[¶]() + +
+Celeb-A Faces dataset +
+ + +```python + +# 우리가 설정한 대로 이미지 데이터셋을 불러와 봅시다 +# 먼저 데이터셋을 만듭니다 +dataset = dset.ImageFolder(root=dataroot, + transform=transforms.Compose([ + transforms.Resize(image_size), + transforms.CenterCrop(image_size), + transforms.ToTensor(), + transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), + ])) +# dataloader를 정의해봅시다 +dataloader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, + shuffle=True, num_workers=workers) + +# GPU 사용여부를 결정해 줍니다 +device = torch.device("cuda:0" if (torch.cuda.is_available() and ngpu > 0) else "cpu") + +# 학습 데이터들 중 몇가지 이미지들을 화면에 띄워봅시다 +real_batch = next(iter(dataloader)) +plt.figure(figsize=(8,8)) +plt.axis("off") +plt.title("Training Images") +plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=2, normalize=True).cpu(),(1,2,0))) + + +``` + +![dataset](./images/dataset.png) + +
+
+ +
+가중치 초기화 +
+ +```python + +# ``netG`` 와 ``netD`` 에 적용시킬 커스텀 가중치 초기화 함수 +def weights_init(m): + classname = m.__class__.__name__ + if classname.find('Conv') != -1: + nn.init.normal_(m.weight.data, 0.0, 0.02) + elif classname.find('BatchNorm') != -1: + nn.init.normal_(m.weight.data, 1.0, 0.02) + nn.init.constant_(m.bias.data, 0) + + +``` + +
+
+ + +--- + +# 생성자 + +G 는 잠재 공간 벡터 z를, 데이터 공간으로 변환시키도록 설계되었다. + +우리에게 데이터라 함은 이미지이기 때문에, z를 데이터공간으로 변환한다는 뜻은, + +학습이미지와 같은 사이즈를 가진 RGB 이미지를 생성하는것과 같다 (예. 3x64x64). + +실제 모델에서는 스트라이드(stride) 2를 가진 전치 합성곱 계층들을 이어서 구성하는데, + +각 전치 합성곱 계층 하나당 2차원 배치 정규화 계층과 relu 활성함수를 한 쌍으로 묶어서 사용한다. + +생성자의 마지막 출력 계층에서는 데이터를 tanh 함수에 통과시키는데, 이는 출력 값을 [−1,1] 사이의 범위로 조정하기 위해서다. + +이때 배치 정규화 계층을 주목할 필요가 있는데, DCGAN 논문에 의하면, 이 계층이 경사하강법(gradient-descent)의 흐름에 중요한 영향을 미치는 것으로 알려져 있다. + +아래의 그림은 DCGAN 논문에서 가져온 생성자의 모델 아키텍쳐다. + +![architecture](./images/architecture.png) + +### 예제 코드[¶]() + +
+Generator +
+ +```python + +# 생성자 코드 + +class Generator(nn.Module): + def __init__(self, ngpu): + super(Generator, self).__init__() + self.ngpu = ngpu + self.main = nn.Sequential( + # 입력데이터 Z가 가장 처음 통과하는 전치 합성곱 계층입니다. + nn.ConvTranspose2d( nz, ngf * 8, 4, 1, 0, bias=False), + nn.BatchNorm2d(ngf * 8), + nn.ReLU(True), + # 위의 계층을 통과한 데이터의 크기. ``(ngf*8) x 4 x 4`` + nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False), + nn.BatchNorm2d(ngf * 4), + nn.ReLU(True), + # 위의 계층을 통과한 데이터의 크기. ``(ngf*4) x 8 x 8`` + nn.ConvTranspose2d( ngf * 4, ngf * 2, 4, 2, 1, bias=False), + nn.BatchNorm2d(ngf * 2), + nn.ReLU(True), + # 위의 계층을 통과한 데이터의 크기. ``(ngf*2) x 16 x 16`` + nn.ConvTranspose2d( ngf * 2, ngf, 4, 2, 1, bias=False), + nn.BatchNorm2d(ngf), + nn.ReLU(True), + # 위의 계층을 통과한 데이터의 크기. ``(ngf) x 32 x 32`` + nn.ConvTranspose2d( ngf, nc, 4, 2, 1, bias=False), + nn.Tanh() + # 위의 계층을 통과한 데이터의 크기. ``(nc) x 64 x 64`` + ) + + def forward(self, input): + return self.main(input) + +# 생성자를 만듭니다 +netG = Generator(ngpu).to(device) + +# 필요한 경우 multi-GPU를 설정 해주세요 +if (device.type == 'cuda') and (ngpu > 1): + netG = nn.DataParallel(netG, list(range(ngpu))) + +# 모든 가중치의 평균을 0( ``mean=0`` ), 분산을 0.02( ``stdev=0.02`` )로 초기화하기 위해 +# ``weight_init`` 함수를 적용시킵니다 +netG.apply(weights_init) + +# 모델의 구조를 출력합니다 +print(netG) + + +``` + +
+
+ +
+Print +
+ +```plaintext + +Generator( + (main): Sequential( + (0): ConvTranspose2d(100, 512, kernel_size=(4, 4), stride=(1, 1), bias=False) + (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) + (2): ReLU(inplace=True) + (3): ConvTranspose2d(512, 256, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False) + (4): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) + (5): ReLU(inplace=True) + (6): ConvTranspose2d(256, 128, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False) + (7): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) + (8): ReLU(inplace=True) + (9): ConvTranspose2d(128, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False) + (10): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) + (11): ReLU(inplace=True) + (12): ConvTranspose2d(64, 3, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False) + (13): Tanh() + ) +) + + +``` + +
+
+ +# 구분자 + +앞서 언급했듯, 구분자 D는 입력 이미지가 진짜 이미지인지 (혹은 반대로 가짜 이미지인지) 판별하는 전통적인 이진 분류 신경망으로 볼 수 있다. + +이때 D는 3x64x64 이미지를 입력받아, Conv2d, BatchNorm2d, 그리고 LeakyReLU 계층을 통과시켜 데이터를 가공시키고, + +마지막 출력에서 Sigmoid 함수를 이용하여 0~1 사이의 확률값으로 조정한다. + +이 아키텍쳐는 필요한 경우 더 다양한 레이어를 쌓을 수 있지만, 배치 정규화와 LeakyReLU, 특히 보폭이 있는 (strided) 합성곱 계층을 사용하는 것에는 이유가 있다. + +DCGAN 논문에서는 보폭이 있는 합성곱 계층을 사용하는 것이 신경망 내에서 스스로의 풀링(Pooling) 함수를 학습하기 때문에, + +데이터를 처리하는 과정에서 직접적으로 풀링 계층( MaxPool or AvgPooling)을 사용하는 것보다 더 유리하다. + +또한 배치 정규화와 leaky relu 함수는 학습과정에서 G와 D가 더 효과적인 경사도(gradient)를 얻을 수 있다. + +
+Discriminator +
+ +```python + +# 구분자 코드 + +class Discriminator(nn.Module): + def __init__(self, ngpu): + super(Discriminator, self).__init__() + self.ngpu = ngpu + self.main = nn.Sequential( + # 입력 데이터의 크기는 ``(nc) x 64 x 64`` 입니다 + nn.Conv2d(nc, ndf, 4, 2, 1, bias=False), + nn.LeakyReLU(0.2, inplace=True), + # 위의 계층을 통과한 데이터의 크기. ``(ndf) x 32 x 32`` + nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False), + nn.BatchNorm2d(ndf * 2), + nn.LeakyReLU(0.2, inplace=True), + # 위의 계층을 통과한 데이터의 크기. ``(ndf*2) x 16 x 16`` + nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False), + nn.BatchNorm2d(ndf * 4), + nn.LeakyReLU(0.2, inplace=True), + # 위의 계층을 통과한 데이터의 크기. ``(ndf*4) x 8 x 8`` + nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False), + nn.BatchNorm2d(ndf * 8), + nn.LeakyReLU(0.2, inplace=True), + # 위의 계층을 통과한 데이터의 크기. ``(ndf*8) x 4 x 4`` + nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False), + nn.Sigmoid() + ) + + def forward(self, input): + return self.main(input) + +# 구분자를 만듭니다 +netD = Discriminator(ngpu).to(device) + +# 필요한 경우 multi-GPU를 설정 해주세요 +if (device.type == 'cuda') and (ngpu > 1): + netD = nn.DataParallel(netD, list(range(ngpu))) + +# 모든 가중치의 평균을 0( ``mean=0`` ), 분산을 0.02( ``stdev=0.02`` )로 초기화하기 위해 +# ``weight_init`` 함수를 적용시킵니다 +netD.apply(weights_init) + +# 모델의 구조를 출력합니다 +print(netD) + + +``` + +
+
+ +
+Print +
+ +```plaintext + +Discriminator( + (main): Sequential( + (0): Conv2d(3, 64, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False) + (1): LeakyReLU(negative_slope=0.2, inplace=True) + (2): Conv2d(64, 128, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False) + (3): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) + (4): LeakyReLU(negative_slope=0.2, inplace=True) + (5): Conv2d(128, 256, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False) + (6): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) + (7): LeakyReLU(negative_slope=0.2, inplace=True) + (8): Conv2d(256, 512, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False) + (9): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True) + (10): LeakyReLU(negative_slope=0.2, inplace=True) + (11): Conv2d(512, 1, kernel_size=(4, 4), stride=(1, 1), bias=False) + (12): Sigmoid() + ) +) + + +``` + +
+
+ + + + +
+loss function, optimizer +
+ +```python + +# ``BCELoss`` 함수의 인스턴스를 초기화합니다 +criterion = nn.BCELoss() + +# 생성자의 학습상태를 확인할 잠재 공간 벡터를 생성합니다 +fixed_noise = torch.randn(64, nz, 1, 1, device=device) + +# 학습에 사용되는 참/거짓의 라벨을 정합니다 +real_label = 1. +fake_label = 0. + +# G와 D에서 사용할 Adam옵티마이저를 생성합니다 +optimizerD = optim.Adam(netD.parameters(), lr=lr, betas=(beta1, 0.999)) +optimizerG = optim.Adam(netG.parameters(), lr=lr, betas=(beta1, 0.999)) + + +``` + +
+
+ +
+Train +
+ +```python + +# 학습 과정 + +# 학습상태를 체크하기 위해 손실값들을 저장합니다 +img_list = [] +G_losses = [] +D_losses = [] +iters = 0 + +print("Starting Training Loop...") +# 에폭(epoch) 반복 +for epoch in range(num_epochs): + # 한 에폭 내에서 배치 반복 + for i, data in enumerate(dataloader, 0): + + ############################ + # (1) D 신경망을 업데이트 합니다: log(D(x)) + log(1 - D(G(z)))를 최대화 합니다 + ########################### + ## 진짜 데이터들로 학습을 합니다 + netD.zero_grad() + # 배치들의 사이즈나 사용할 디바이스에 맞게 조정합니다 + real_cpu = data[0].to(device) + b_size = real_cpu.size(0) + label = torch.full((b_size,), real_label, + dtype=torch.float, device=device) + # 진짜 데이터들로 이루어진 배치를 D에 통과시킵니다 + output = netD(real_cpu).view(-1) + # 손실값을 구합니다 + errD_real = criterion(output, label) + # 역전파의 과정에서 변화도를 계산합니다 + errD_real.backward() + D_x = output.mean().item() + + ## 가짜 데이터들로 학습을 합니다 + # 생성자에 사용할 잠재공간 벡터를 생성합니다 + noise = torch.randn(b_size, nz, 1, 1, device=device) + # G를 이용해 가짜 이미지를 생성합니다 + fake = netG(noise) + label.fill_(fake_label) + # D를 이용해 데이터의 진위를 판별합니다 + output = netD(fake.detach()).view(-1) + # D의 손실값을 계산합니다 + errD_fake = criterion(output, label) + # 역전파를 통해 변화도를 계산합니다. 이때 앞서 구한 변화도에 더합니다(accumulate) + errD_fake.backward() + D_G_z1 = output.mean().item() + # 가짜 이미지와 진짜 이미지 모두에서 구한 손실값들을 더합니다 + # 이때 errD는 역전파에서 사용되지 않고, 이후 학습 상태를 리포팅(reporting)할 때 사용합니다 + errD = errD_real + errD_fake + # D를 업데이트 합니다 + optimizerD.step() + + ############################ + # (2) G 신경망을 업데이트 합니다: log(D(G(z)))를 최대화 합니다 + ########################### + netG.zero_grad() + label.fill_(real_label) # 생성자의 손실값을 구하기 위해 진짜 라벨을 이용할 겁니다 + # 우리는 방금 D를 업데이트했기 때문에, D에 다시 가짜 데이터를 통과시킵니다. + # 이때 G는 업데이트되지 않았지만, D가 업데이트 되었기 때문에 앞선 손실값가 다른 값이 나오게 됩니다 + output = netD(fake).view(-1) + # G의 손실값을 구합니다 + errG = criterion(output, label) + # G의 변화도를 계산합니다 + errG.backward() + D_G_z2 = output.mean().item() + # G를 업데이트 합니다 + optimizerG.step() + + # 훈련 상태를 출력합니다 + if i % 50 == 0: + print('[%d/%d][%d/%d]\tLoss_D: %.4f\tLoss_G: %.4f\tD(x): %.4f\tD(G(z)): %.4f / %.4f' + % (epoch, num_epochs, i, len(dataloader), + errD.item(), errG.item(), D_x, D_G_z1, D_G_z2)) + + # 이후 그래프를 그리기 위해 손실값들을 저장해둡니다 + G_losses.append(errG.item()) + D_losses.append(errD.item()) + + # fixed_noise를 통과시킨 G의 출력값을 저장해둡니다 + if (iters % 500 == 0) or ((epoch == num_epochs-1) and (i == len(dataloader)-1)): + with torch.no_grad(): + fake = netG(fixed_noise).detach().cpu() + img_list.append(vutils.make_grid(fake, padding=2, normalize=True)) + + iters += 1 + + +``` + +
+
+ +
+Result +
+ +```python + +plt.figure(figsize=(10,5)) +plt.title("Generator and Discriminator Loss During Training") +plt.plot(G_losses,label="G") +plt.plot(D_losses,label="D") +plt.xlabel("iterations") +plt.ylabel("Loss") +plt.legend() +plt.show() + + +# G의 학습과정 시각화 +fig = plt.figure(figsize=(8,8)) +plt.axis("off") +ims = [[plt.imshow(np.transpose(i,(1,2,0)), animated=True)] for i in img_list] +ani = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=1000, blit=True) + +HTML(ani.to_jshtml()) + + +# 진짜 이미지 vs 가짜 이미지 + +# dataloader에서 진짜 데이터들을 가져옵니다 +real_batch = next(iter(dataloader)) + +# 진짜 이미지들을 화면에 출력합니다 +plt.figure(figsize=(15,15)) +plt.subplot(1,2,1) +plt.axis("off") +plt.title("Real Images") +plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=5, normalize=True).cpu(),(1,2,0))) + +# 가짜 이미지들을 화면에 출력합니다 +plt.subplot(1,2,2) +plt.axis("off") +plt.title("Fake Images") +plt.imshow(np.transpose(img_list[-1],(1,2,0))) +plt.show() + + +``` + +![fation-mnist](./images/output1.png.jpg) +![fation-mnist](./images/output2.png.jpg) +![fation-mnist](./images/output3.png.jpg) +![fation-mnist](./images/output10.png.jpg) + + +
+
+ + +--- + +### 참고[¶]() + +- GAN - Google +- ChatGPT +- [파이토치 GAN 튜토리얼](https://tutorials.pytorch.kr/beginner/dcgan_faces_tutorial.html)