본문 바로가기

딥러닝/개인구현 정리

[DeepLearning] GAN을 활용한 새로운 산타클로스 얼굴 만들기

320x100

 

산타가 오는 겨울이 지나고, 많은 인파들 사이에 숨어버렸다고 합니다.

숨어있는 산타의 얼굴을 찾아 새로운 산타를 
만들어 볼까요?

 

 

이번 신경망에서도 GAN(생성 적대적 신경망)을 사용할 예정입니다.

 

오늘 가져올 kaggle 데이터는 Is that santa? (Image Classification) 데이터셋으로

데이터 자체는 분류모델을 통해 실제 santa와 산타로 위장한(전혀 다르게 생길때도 있음) not a santa를 찾는 

데이터셋 입니다. 다만 not a santa는 RNN이나 Resnet을 활용해서 많은 분들이 접근하고 있는 코드이기에

필자는 따로 train에서 산타 이미지만 추출해서 새로운 이미지를 만들어볼 계획입니다.

 

실제 데이터셋 (kaggle)

 

IS THAT SANTA? (Image Classification) | Kaggle

 

IS THAT SANTA? (Image Classification)

Santa Claus Classification

www.kaggle.com

 

그리고 학습을 완료한 후에 한꺼번에 돌릴 수 있도록 animation 패키지

animation.ArtistAnimation 라이브러리도 사용할 예정입니다.

 


 

이번 학습에 사용할 라이브러리부터 로드 합니다.

파이토치부터 시각화를 위한 맷플롯립, Ipython 관련 패키지, os, glob, zipfile도 포함되어 있습니다.

 

# pytorch 라이브러리
import torch
import torchvision
import torch.nn.functional as F
from torch.utils import data
from torch import nn, optim
from torchvision import transforms, datasets
from torch.utils.data import Dataset, DataLoader
from torchvision import models
from torchvision import utils
from torchvision.utils import save_image
from torchvision.datasets import ImageFolder
from transformers import PreTrainedTokenizerFast
import torchvision.models
from torchsummary import summary

# 시각화 관련
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import matplotlib as mpl
from matplotlib import cm 
from tqdm.notebook import tqdm
from PIL import Image

# 영상관련
from IPython.display import HTML
import matplotlib.animation as animation

# arange 기본
import numpy as np
import pandas as pd

# 그외
import os
import random
import tqdm
from glob import glob
import zipfile
import imageio
import os

 

gpu를 사용합니다.

 

USE_CUDA = torch.cuda.is_available()
device= torch.device("cuda" if USE_CUDA else "cpu")
print(f"Using Device : {device}")

 

캐글에서 json파일을 불러와서 이번 사용할 데이터셋도 다운로드 합니다.

 

# 캐글 json으로 산타클로스 데이터 불러오기
from google.colab import files
!pip install -q kaggle #kaggle 설치 # --quiet
# -- kaggle api를 사용하기 위한 인증파일을 설정 (8-11)
files.upload() #kaggle API file upload
!mkdir ~/.kaggle # kaggle 디렉토리 생성 / mkdir : make directory - 폴더 생성 (~)
!cp kaggle.json ~/.kaggle/ #kaggle.json 파일 kaggle 폴더에 복사 / cp a b (copy)
!chmod 600 ~/.kaggle/kaggle.json # 권한 변경 r w x (4 2 1)
# ---------
# https://www.kaggle.com/datasets/deepcontractor/is-that-santa-image-classification
!kaggle datasets download -d deepcontractor/is-that-santa-image-classification
Downloading is-that-santa-image-classification.zip to /content
 99% 201M/203M [00:07<00:00, 37.6MB/s]
100% 203M/203M [00:07<00:00, 29.0MB/s]

 

json을 사용하는 방법은 이전 게시물의 초반부에서도 설명되어 있군요.

 

[DeepLearning] 이미지 구분 모델_Pokemon 809 세트_ep.1 (tistory.com)

 

[DeepLearning] 이미지 구분 모델_Pokemon 809 세트_ep.1

이번시간에는 Kaggle의 데이터셋 중 pokemon image dataset을 활용하여, 이미지를 구분하는 모델을 생성하도록 하겠습니다. Pokemon Image Dataset | Kaggle Pokemon Image Dataset Pokemon image dataset www.kaggle.com 데이터는

astart.tistory.com

 

이렇게 되면 코랩에 압축파일인 상태로 저장되있을 것입니다.

!unzip으로 압축을 해제합니다.

 

!unzip /content/is-that-santa-image-classification.zip

 

본 데이터셋은 train 폴더에 santa와 not a santa 폴더, 

test 폴더에 santa와 not a santa 폴더로 구분되어 있답니다.

 

test not-a-santa 일반인 이미지
santa 산타 이미지
train not-a-santa 일반인 이미지
santa 산타 이미지

이런식으로 구성되어 있습니다.

 

실질적으로 산타가 어떤것인지는 한번 보겠습니다. 

차후에 이런 구분을 사용할 일은 없을 것입니다.

 

# 산타 이미지만 불러오기 

train_mother_path = '/content/is that santa/train/'
test_mother_path = '/content/is that santa/test/'

train_image_path = glob(os.path.join(train_mother_path, 'santa', '*'))
test_image_path = glob(os.path.join(test_mother_path, 'santa', '*'))

idx = np.random.randint(0, len(train_image_path))
image = Image.open(train_image_path[idx])
plt.imshow(image)
plt.title(image.size)
plt.show()

 

이런식으로 산타가 존재합니다.

붉은색 상의, 붉은색 모자, 긴 흰수염을 갖고 있는 남성이라면 거의 나오는 상황이고

한 명이거나 다수의 산타, 산타와 다른 인물이 함께 있을때도 있구요.

 

사진이 아닌 벡터 이미지나 만화캐릭터, 인형 비슷한 것이 등장할 때도 있습니다.

 

# 테스트 폴더에서는 비 산타만 불러오기
test_image_path = glob(os.path.join(test_mother_path, 'not-a-santa', '*'))

idx = np.random.randint(0, len(test_image_path))
image = Image.open(test_image_path[idx])
plt.imshow(image)
plt.title(image.size)
plt.show()

 

 

not a santa 항목에는 정말 다양한 사진들이 들어있습니다. 서양인, 동양인, 고양이, 사물 등등.

산타와 유사점이 없는 인물들이 대다수이고, 간혹 붉은색 옷, 흰색 수염,

할아버지 등 한두가지 정도 산타의 특징을 갖고 있는 인물이 있기도 합니다.

 

여기서는 초상권 상 트리 이미지를 뽑아봤습니다.

 

transforms을 사용해서 이미지를 64 * 64 이미지로 잘라봅니다.

 

# 트랜스폼

dataset = torchvision.datasets.ImageFolder(
    root = "/content/is that santa/train", 
    transform = transforms.Compose(
        [transforms.Resize((64, 64)), #이미지 사이즈도 수정이 필요하지 않을까? 초기 값 = 64
         transforms.CenterCrop(64), # 삭제시 8*8개씩 이미지 시각화
         transforms.ToTensor(),
         transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))]))

 

데이터셋 형태 확인

dataset
Dataset ImageFolder
    Number of datapoints: 614
    Root location: /content/is that santa/train
    StandardTransform
Transform: Compose(
               Resize(size=(64, 64), interpolation=bilinear, max_size=None, antialias=None)
               CenterCrop(size=(64, 64))
               ToTensor()
               Normalize(mean=(0.5, 0.5, 0.5), std=(0.5, 0.5, 0.5))
           )

 

생성자를 정의해보겠습니다. ConvTranspose2d를 사용했고, 배치 정규화도 진행,

이미지 사이즈는 위에서 따로 지정해줍니다.

GAN의 경우 활성화함수는 LeakyReLU가 효율이 좋기 때문에 생성자에서 사용합니다.

 

 

# 잠재 벡터 사이즈 잠재벡터 = 입력 데이터에서 중요한 의미만을 남겨 놓은 것
n_z = 100
# 이미지 사이즈
n_f = 64
# 채널 사이즈
n_c = 3

class Generator(nn.Module):
    def __init__(self, n_z, n_f, n_c, relu_slope=0.2):
        super().__init__()
        
        self.network = nn.Sequential(nn.ConvTranspose2d(in_channels=n_z, out_channels=n_f*8, kernel_size=4, stride=1, padding=0, bias=False),
                                     nn.BatchNorm2d(n_f*8),
                                     nn.LeakyReLU(negative_slope=relu_slope,inplace=True),
                                     nn.ConvTranspose2d(in_channels=n_f*8, out_channels=n_f*4, kernel_size=4, stride=2, padding=1, bias=False),
                                     nn.BatchNorm2d(n_f*4),
                                     nn.LeakyReLU(negative_slope=relu_slope,inplace=True),
                                     nn.ConvTranspose2d(in_channels=n_f*4, out_channels=n_f*2, kernel_size=4, stride=2, padding=1, bias=False),
                                     nn.BatchNorm2d(n_f*2),
                                     nn.LeakyReLU(negative_slope=relu_slope,inplace=True),
                                     nn.ConvTranspose2d(in_channels=n_f*2, out_channels=n_f, kernel_size=4, stride=2, padding=1, bias=False),
                                     nn.BatchNorm2d(n_f),
                                     nn.LeakyReLU(negative_slope=relu_slope,inplace=True),
                                     nn.ConvTranspose2d(in_channels=n_f, out_channels=n_c, kernel_size=4, stride=2, padding=1, bias=False),
                                     nn.Tanh()
                                    )
    def forward(self, x):
        return self.network(x)

 

합성곱으로 바꿔서 판별자도 정의해줍니다.

이는 다음 셀에서 형태를 조회할 것입니다.

 

class Discriminator(nn.Module):
    def __init__(self, n_f, n_c, relu_slope=0.2):
        super().__init__()
        self.network = nn.Sequential(nn.Conv2d(in_channels=n_c, out_channels=n_f, kernel_size=4, stride=2, padding=1, bias=False),
                                     nn.LeakyReLU(negative_slope=relu_slope,inplace=True),
                                     nn.Conv2d(in_channels=n_f, out_channels=n_f*2, kernel_size=4, stride=2, padding=1, bias=False),
                                     nn.BatchNorm2d(n_f*2),
                                     nn.LeakyReLU(negative_slope=relu_slope,inplace=True),
                                     nn.Conv2d(in_channels=n_f*2, out_channels=n_f*4, kernel_size=4, stride=2, padding=1, bias=False),
                                     nn.BatchNorm2d(n_f*4),
                                     nn.LeakyReLU(negative_slope=relu_slope,inplace=True),
                                     nn.Conv2d(in_channels=n_f*4, out_channels=n_f*8, kernel_size=4, stride=2, padding=1, bias=False),
                                     nn.BatchNorm2d(n_f*8),
                                     nn.LeakyReLU(negative_slope=relu_slope,inplace=True),
                                     nn.Conv2d(in_channels=n_f*8, out_channels=1, kernel_size=4, stride=1, padding=0, bias=False),
                                     nn.Sigmoid()
                                    )
    def forward(self, x):
        return self.network(x)

 

generator와 discriminator의 형태

 

# 두 모델 
gen = Generator(n_z, n_f, n_c)
print('Generator================')
print(gen)
dis = Discriminator(n_f, n_c)
print('Discriminator================')
print(dis)

 

Generator================
Generator(
  (network): 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): LeakyReLU(negative_slope=0.2, 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): LeakyReLU(negative_slope=0.2, 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): LeakyReLU(negative_slope=0.2, 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): LeakyReLU(negative_slope=0.2, inplace=True)
    (12): ConvTranspose2d(64, 3, kernel_size=(4, 4), stride=(2, 2), padding=(1, 1), bias=False)
    (13): Tanh()
  )
)
Discriminator================
Discriminator(
  (network): 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()
  )
)

 

생성자는 열심히 train폴더의 산타 이미지를 보고 fake 산타를 만들어내고, 판별자는

열심히 산타 이미지를 잡아낼 것입니다.

그러면 다시 생성자는 열심히 판별자를 피해서 산타 이미지를 만들어 내겠죠?

 

 

가중치 초기화용 함수 선언

 

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)

 

시드값 정의와 생성자&판별자 gpu에 입력, 가중치 초기화를 정의합니다.

노이즈와 배치 사이즈, 데이터로더까지 정의합니다.

 

# 랜덤 시드
torch.manual_seed(1)
torch.cuda.manual_seed(1)
torch.backends.cudnn.deterministic = True
torch.use_deterministic_algorithms = True

# 생성자 판별자
dis = Discriminator(n_f, n_c).to(device)
gen = Generator(n_z, n_f, n_c).to(device)

# 가중치 
dis.apply(weights_init)
gen.apply(weights_init)

# 시각화
fixed_noise = torch.randn(64, n_z, 1, 1, device=device)

# dataloader - 배치 사이즈 조정이 필요하지 않을까? - 초기 16
batch_size = 32 # 32, 64도 시도중
dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False, drop_last=True)

 

로스 함수와 옵티마이저(최적화)는 바로 위 셀과 한 셀로 넣어도 무방합니다.

지난번처럼 보상과 벌을 확실하게 주기 때문에 학습에 도움을 주는 BCEloss(binary cross entropy)만 사용합니다.

(패러미터 조정을 위해 따로 빼놓음)

 

# 로스 함수 설정
criterion = nn.BCELoss()

# 옵티마이저 설정 (lr 조정중) (Discrminator가 더 낮게 줄어들어야 할 것) (lr을 똑같이 맞춰야 할까?)
optimizer_d = optim.Adam(dis.parameters(), lr=0.001, betas=(0.9, 0.999))
optimizer_g = optim.Adam(gen.parameters(), lr=0.001, betas=(0.9, 0.999))
# optimizer_d = optim.Adam(dis.parameters(), lr=0.001)
# optimizer_g = optim.Adam(gen.parameters(), lr=0.001)

# 각종 변수 리스트들
img_list = []
g_loss = []
d_loss = []
iters = 0

 

이제 학습을 시작합니다.

epoch는 10 → 20 → 100회까지 진행하다가 일단 100회로 꾸준히 넣어보았고

시간이 남을 경우 200회까지 진행하는 것이 합리적일 것으로 판단하고 있습니다.

 

from tqdm import tqdm

# epoch
num_epochs = 100

# epoch 시작
# 시간 체크
for epoch in tqdm(range(num_epochs)):
    for i, batch in enumerate(dataloader):
        
        # 식별자 
        dis.train()
        gen.train()
        X, _ = batch
        X = X.to(device)
        dis.zero_grad()
        
        label = torch.full((X.shape[0],),1.,dtype=torch.float,device=device)
        output = dis(X).view(-1)
        errD_real = criterion(output, label)
        errD_real.backward()
        D_x = output.mean().item()


        noise = torch.randn(batch_size, n_z, 1, 1, device=device)
        fake = gen(noise)
        label.fill_(0.)
        output = dis(fake.detach()).view(-1)
        errD_fake = criterion(output, label)
        errD_fake.backward()
        D_G_z1 = output.mean().item()
        errD = errD_real + errD_fake
        optimizer_d.step()

        # 판별자 업데이트
        gen.zero_grad()
        label.fill_(1)  
        output = dis(fake).view(-1)
        errG = criterion(output, label)
        errG.backward()
        D_G_z2 = output.mean().item()
        # 생성자 업데이트
        optimizer_g.step()

        if i % 10 == 0:
            print('[%d/%d][%d/%d]\tLoss_D: %.5f\tLoss_G: %.5f\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_loss.append(errG.item())
        d_loss.append(errD.item())

        dis.eval()
        gen.eval()

    with torch.no_grad():
        fake = gen(fixed_noise).detach().cpu()
    img_list.append(torchvision.utils.make_grid(fake, padding=2, normalize=True))

 

tqdm 사용으로 epoch 과정을 로딩 게이지처럼 표현합니다.

 

  0%|          | 0/100 [00:00<?, ?it/s]

 

 50%|█████     | 50/100 [09:42<09:28, 11.37s/it]
 
현재 50% 정도 진행했을 때의 시간을 예상해보면 약 19분 정도 걸리는군요.
 
100%|██████████| 100/100 [19:05<00:00, 11.45s/it]
 

완료. 100회시의 판별자 loss, 생성자 loss는 다음과 같습니다.

 

[99/100][0/19]	Loss_D: 0.05917	Loss_G: 6.29481	D(x): 0.9765	D(G(z)): 0.0302 / 0.0092
[99/100][10/19]	Loss_D: 0.34362	Loss_G: 4.16688	D(x): 0.8092	D(G(z)): 0.0203 / 0.0649

 

loss를 그래프로 표현합니다.

Generator는 산타를 상징한다는 뜻해서 빨간색, Discriminator는 파란색으로 표기했습니다.

 

# 시각화

fig, axes = plt.subplots(1,1,figsize=(10,6))
axes.plot(d_loss,label="Discriminator", color='blue')
plt.legend()
axes.plot(g_loss,label="Generator",color='red')
plt.legend()
axes.set_title('Loss curve',size=16)
axes.set_xlabel('Iterations',size=16)
axes.set_ylabel('Loss',size=16)
 

 
충분히 학습이 되었는지 살펴보기 위해서, 이미지를 꺼내보겠습니다.
 
fig = plt.figure(figsize=(8,7))
plt.axis("off")
ims = [[plt.imshow(np.transpose(i,(1,2,0)), animated=False)] for i in img_list]
ani = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=1000, blit=True)

HTML(ani.to_jshtml())

가장 높았을 때의 화면

 

 

아직 만족스럽게 학습되진 않은 상태입니다.

 

패러미터 몇가지 조정을 해보겠습니다.

 

optimizer_d = optim.Adam(dis.parameters(), lr=0.001, betas=(0.5, 0.999))
optimizer_g = optim.Adam(gen.parameters(), lr=0.001, betas=(0.5, 0.999))

 

최적화 함수인 Adam에서 betas값은 기울기와 기울기 제곱의 실행시 평균을 계산하는데

사용되는 계수(그냥 숫자)입니다. betas 계수를 약간 낮춥니다.

 

num_epochs = 200

 

화끈하게 epoch 100말고 epoch 200을 태워보겠습니다.

(35분 소요)

 

from tqdm import notebook

===========================
중략

for epoch in notebook.tqdm(range(num_epochs)):

 

이 학습에서는 지루함을 덜기 위해 tqdm의 notebook도 사용해봅니다.

게이지가 흑백이 아닌 파란색으로 표기되네요.

게임 로딩같고 좋습니다.

 

epoch 종료후, loss 함수에 대해 확인해봤습니다.

 

Text(0, 0.5, 'Loss')

 

인터벌이 모두 끝나기 직전 이미지입니다.

 

얼굴의 형태가 얼핏 드러나네요.
마지막으로 제출된 200회 완료시 이미지입니다.

 

 

확실히 1~3열까지는 사람의 신체 형태에 붉은색 옷, 흰 수염이 달린 것이 보입니다.

형태를 유지하는 것이 보이는군요. 확연히 나아졌습니다.

 

 

새로운 산타 생성은 여기까지 입니다!

 

 


2023.02.10 추가

 

EPOCH 150회정도로 50회만 낮추면 어떻게 되는지 확인해보기 위해 

데이터를 다시 돌렸습니다.

 

 

Red 컬러가 약간 줄어들고, 얼굴부분이 더 없어졌네요.

728x90