Deep Learning through deep learning

#2 GAN (Generative Adversarial Networks) - MNIST, CelebA, 자체 제작 데이터셋 실습 본문

ML&DL/GAN

#2 GAN (Generative Adversarial Networks) - MNIST, CelebA, 자체 제작 데이터셋 실습

NeuroN 2023. 8. 1. 18:47

본격적으로 GAN에 대해 알아가보자.

해당 글은 이미 널리다시피 널린 GAN에 대한 이론정보를 이미 어느정도 파악한 후 본격적으로 구현하고자 하는 의지를 가진 사람들을 위해 작성되었다.

GAN 의 이론을 빠르게 훑자. 해당 글의 목적은 활용에 있지 구현에 있지 않다는 것을 명시하고 간다.


Theory

  1. GAN은 당연하게도 기존의 인공신경망(ANN)의 구조에서 시작된다.
  2. Multi Layer Perceptron 을 예시로 들자면, MLP에서는 이미지를 입력으로 넣어 클래스에 대한 정보를 출력해냈다면, GAN에서는 출력된 정보를 입력으로 넣어 이미지를 뽑아내는 백쿼리 이론을 따른다.
  3. GAN 에서는 적대적 훈련이라고 부르는 Discriminator, Generator 두 신경망이 존재하는데, 쉽게 말해 G에서는 백쿼리의 이론처럼 이미지를 뽑아내고(생성해내고) D에서는 G가 생성해낸 이미지와 원래 정답 이미지를 구별해낸다.
  4. D가 생성된 이미지와 원래 이미지를 잘 구분할수록 G는 더욱 열심히 실제와 같은 이미지를 만들어내려고 노력할 것이다. 이는 결국 실제와 같은 이미지 생성이 가능하다는 결론이다.

Using GAN

  • 우리는 앞선 이론을 대강 파악함으로써 GAN 모델이  '실제와 같은 이미지 생성' 을 한다는걸 짐작할 수 있다.
  • #1 의 이론에서는 GAN, WGAN, WGAN-GP, LSGAN 과 같은 다양한 모델에 대해 소개를 하였다.
    • 간략하게 소개하자면 GAN을 제외한 이 모델들은 안정적인 훈련과 고품질 이미지 생성을 위한 GAN 아키텍쳐이다.
    • 즉, GAN을 포함한 이 모델들도 전부 '실제와 같은 이미지 생성' 이라는 같은 결과를 출력한다는 점에서 알고리즘 최적화라고 볼 수 있다.
  • 본격적으로 프로그래밍에 들어가기 앞서 다룰 내용에 대해 소개하고자 한다.
    1. Mnist 손글씨의 csv 파일을 가져다가 GAN을 훈련시켜보고 결과를 볼것이다.
    2. Celeba 얼굴이미지의 csv 파일을 가져다가 GAN을 훈련시켜보고 결과를 볼것이다.
    3. 직접 만든 데이터셋을 가지고 GAN을 훈련시켜보고 결과를 볼것이다.
    4. DCGAN ...

Coding GAN

MNIST 손글씨 데이터 (CSV 파일 형식)를 활용한 GAN

더보기

1. 먼저 MNIST 손글씨 데이터가 담긴 csv 파일을 다운로드 받는다.

mnist_train.zip
12.66MB

2. 첫 GAN 이므로 간단하게 전체 구조에 대해 코드 설명을 들어간다.

import torch
import torchvision
if torch.cuda.is_available():
  torch.set_default_tensor_type(torch.cuda.FloatTensor)
  print("using cuda: ", torch.cuda.get_device_name(0))
  pass
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
device

> GPU 설정 부분이다. 본인의 컴퓨터에 GPU 사용여부를 체크하여 cuda 혹은 cpu 라고 뜰것이며, gpu를 사용하는것이 빠른 학습이 가능하므로 추천한다.

import torch
import torch.nn as nn
from torch.utils.data import Dataset
import pandas, numpy, random
import matplotlib.pyplot as plt
from torch.utils.data import Dataset

class MnistDataset(Dataset):
  def __init__(self,csv_file):
    self.data_df = pandas.read_csv(csv_file, header=None)
    pass

  def __len__(self):
    return len(self.data_df)

  def __getitem__(self,index):
    label = self.data_df.iloc[index,0]
    target = torch.zeros((10))
    target[label] = 1.0
    image_values = torch.cuda.FloatTensor(self.data_df.iloc[index,1:].values)/255.0
    return label, image_values, target

  def plot_image(self, index):
    img = self.data_df.iloc[index,1:].values.reshape(28,28)
    plt.title("label = "+str(self.data_df.iloc[index,0]))
    plt.imshow(img.astype('float'), interpolation='none', cmap='Blues')
    pass
  pass

mnist_dataset = MnistDataset('./mnist_train.csv')
mnist_dataset.plot_image(1) # 이미지가 정상적으로 잘 나오는지 확인

> 다운받은 MNIST 손글씨의 데이터셋 csv파일을 불러오는 구간이다. mnist_dataset 부분에 ' ' 사이 경로를 본인이 다운받은 경로로 압축을 해제한 후  넣으면된다.

class Discriminator(nn.Module):
  def __init__(self):
    super().__init__()
    
    self.model = nn.Sequential(
        nn.Linear(784,200),
        nn.LeakyReLU(0.02),
        nn.LayerNorm(200),
        nn.Linear(200,1),
        nn.Sigmoid())
        
    self.loss_function = nn.BCELoss()
    
    self.optimiser = torch.optim.Adam(self.parameters(), lr = 0.01)

    self.counter = 0
    self.progress = []
    pass
  
  def forward(self, inputs):
    return self.model(inputs)
    
  def train(self,inputs,targets):
    outputs = self.forward(inputs)
    loss = self.loss_function(outputs,targets)
    self.counter +=1
    if(self.counter%10==0):
      self.progress.append(loss.item())
      pass
    if(self.counter%10000==0):
      print("counter = ",self.counter)
      pass
    
    self.optimiser.zero_grad()
    loss.backward()
    self.optimiser.step()
    pass
  
  def plot_progress(self):
    df = pandas.DataFrame(self.progress, columns=['loss'])
    df.astype('float').plot(ylim=(0,1.0), figsize=(16,8), alpha=0.1, marker='.', grid=True, yticks=(0,0.25,0.5))
    pass

> 판별기 Discriminator 를 구현한 코드이다.

init 에서는 model을 정의해주고, loss function 및 optimizer인 파라미터들을 정의해준다. model은 기본적인 MLP 구조임을 확인할 수 있다.

forward 에서는 순전파

train 에서는 기존의 MLP와 같은 형식으로 함수가 정의되어 MNIST 손글씨를 구분한다.

plot_progress 에서는 MNIST 손글씨의 이미지를 읽어 시각화할 수 있게 해준다.

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

    self.model = nn.Sequential(
        nn.Linear(100,200),
        nn.LeakyReLU(0.02),
        nn.LayerNorm(200),
        nn.Linear(200,784),
        nn.Sigmoid())

    self.optimiser = torch.optim.Adam(self.parameters(), lr = 0.01)
    self.counter = 0
    self.progress = []
    pass
  
  def forward(self, inputs):
    return self.model(inputs)

  def train(self,D,inputs,targets):
    g_output = self.forward(inputs)
    d_output = D.forward(g_output)
    loss = D.loss_function(d_output,targets)
    self.counter +=1
    if(self.counter%10==0):
      self.progress.append(loss.item())
      pass
    
    self.optimiser.zero_grad()
    loss.backward()
    self.optimiser.step()
    pass

  def plot_progress(self):
    df = pandas.DataFrame(self.progress, columns=['loss'])
    df.astype('float').plot(ylim=(0,1.0), figsize=(16,8), alpha=0.1, marker='.', grid=True, yticks=(0,0.25,0.5))
    pass

> 생성기 Generator 를 구현한 코드이다.

init 에서는 D와 마찬가지로 model, loss function, optimizer를 정의하는데, D와 달리 model의 구조가 정반대임을 확인할 수 있다. D는 이미지를 입력으로 클래스를 분류하지만, G는 클래스 정보를 입력으로 이미지를 출력해내기 위해서이다.

forward는 D와 같다.

train 도 D와 유사하지만, 손실인 loss 부분이 D에게서 온다는 차이점이 있다. (이는 이론을 참고)

plot 도 D와 같다.

def generate_random_seed(size):
  random_data = torch.randn(size)
  return random_data
  
D = Discriminator()
G = Generator()
image_list=[]
D.to(device)
G.to(device)

for i in range(20):
  for label, image_data_tensor, target_tensor in mnist_dataset:
    #1단계
    D.train(image_data_tensor, torch.cuda.FloatTensor([1.0]))
	#2단계
    D.train(G.forward(generate_random_seed(100)).detach(), torch.cuda.FloatTensor([0.0]))
	#3단계
    G.train(D, generate_random_seed(100), torch.cuda.FloatTensor([1.0]))

> 모델을 훈련시키는 코드이다.

기본적인 GAN 훈련 코드는 아주 간결하다. 먼저, 앞서 만들어준 D,G를 정의해주고 device를 통해 gpu를 할당해준다.

이후 for 반복문을 통해 모델을 훈련시킬 예정이며, 첫번째 for문에 원하는 학습 횟수를 넣으면 된다.

두번째 for문에서는 MNIST의 데이터셋 개수만큼 훈련이 진행된다.

1단계 D.train 을 통해 먼저 참에 대한 판별기를 훈련시켜주고,

2단계 D.train 을 통해 거짓에 대한 판별기를 훈련시켜주고, (이때 detach는 G의 기울기가 수정되지 않도록 한다.)

3단계 G.train 을 통해 생성기를 훈련시켜준다.

def plot_progress(self):
  df = pandas.DataFrame(self.progress, columns=['loss'])
  df.plot(ylim=(0), figsize=(16,8), alpha=0.1, marker='.',grid=True,yticks=(0,0.25,0.5,1.0,5.0))

#1
D.plot_progress()

#2
'''
G.plot_progress()
'''

#3
'''
f, axarr = plt.subplots(2,3,figsize=(16,8))
for i in range(2):
  for j in range(3):
    output = G.forward(generate_random_seed(100))
    img = output.detach().cpu().numpy().reshape(28,28)
    axarr[i,j].imshow(img, interpolation='none', cmap='Blues')
    pass
  pass
'''

> 모델 훈련 결과를 보는 코드이다.

#1에서 D 판별기에 대한 loss 확인이 가능하다.

#2에서 G 판별기에 대한 loss 확인이 가능하다.

#3에서 대략 6개 정도의 생성기 G가 출력해낸 이미지를 확인해볼 수 있다. (개수는 조정가능)

 

결론적으로 데이터셋은 주어졌지만, 원하는 결과는 결국 '실제와 같은 이미지' 이므로 위 결과 코드를 통해 결과 이미지를 이용해볼 수 있다.

CelebA 얼굴이미지 데이터 (이미지 데이터 형식)를 활용한 GAN

더보기

1. 먼저 CelebA 얼굴이미지 데이터가 담긴 이미지 파일을 다운로드 받는다.

용량 한계로 인해 아래의 kaggle 링크를 통해 다운로드 받을 수 있다.

>> https://www.kaggle.com/datasets/jessicali9530/celeba-dataset?select=img_align_celeba <<

다운받은 후 zip 파일 그대로 가지고 있기

 

2. 빠르게 학습 까지의 코드를 살펴본다. 우리의 목적은 직접 데이터셋을 만드는 것이기 때문에 위의 MNIST, CelebA에서 데이터 형식을 참고하여 학습을 진행해보는 것이다.

 

import h5py
import zipfile
import imageio
import os

total_images = 20000

with h5py.File(hdf5_file, 'w') as hf:

    count = 0

    with zipfile.ZipFile('./archive(1).zip', 'r') as zf:
      for i in zf.namelist():
        if (i[-4:] == '.jpg'):
          # extract image
          ofile = zf.extract(i)
          img = imageio.imread(ofile)
          os.remove(ofile)

          hf.create_dataset('img_align_celeba/'+str(count)+'.jpg', data=img, compression="gzip", compression_opts=9)
          
          count = count + 1
          if (count%1000 == 0):
            print("images done .. ", count)
            pass
            
          if (count == total_images):
            break
          pass
        pass
      pass

> 이미지 데이터를 불러오는 코드이다.

total_images 는 우리가 사용할 이미지 데이터 개수이다. CelebA 이미지 데이터셋은 총 20만장의 이미지가 있어 너무 많이 사용하면 학습이 오래걸리기 때문에 원하는 개수만 사용이 가능하도록 하였다.

다운로드 받은 zip 파일 데이터셋을 with zipfile.ZipFile(' ') 안에 경로를 넣는다.

위 코드를 실행하면 20000장의 이미지를 저장하지만, 커널이 종료되면 다시 새로 불러와야한다는 점을 주의하자.

import torch
import torch.nn as nn
from torch.utils.data import Dataset
import pandas, numpy, random
import matplotlib.pyplot as plt
from torch.utils.data import Dataset

class CelebADataset(Dataset):
  def __init__(self,file):
    self.file_object = h5py.File(file, 'r')
    self.dataset = self.file_object['img_align_celeba']
    pass

  def __len__(self):
    return len(self.dataset)

  def __getitem__(self,index):    
    if(index>=len(self.dataset)):
      raise IndexError()
    img = numpy.array(self.dataset[str(index)+'.jpg'])
    return torch.cuda.FloatTensor(img)/255.0

  def plot_image(self, index):
    plt.imshow(numpy.array(self.dataset[str(index)+'.jpg']), interpolation='nearest')
    pass
  pass

> 이미지 데이터 전처리 코드이다.

len 에서는 이미지 데이터 개수(길이)를 가져온다.

getitem 에서는 jpg 이미지를 numpy 배열로 바꾸어 정규화까지 마친 이미지를 불러온다.

plot_image 에서는 이미지를 시각화하여 보여준다.

def generate_random_image(size):
  random_data = torch.rand(size)
  return random_data
  
def generate_random_seed(size):
  random_data = torch.randn(size)
  return random_data
  
class View(nn.Module):
    def __init__(self, shape):
        super().__init__()
        self.shape = shape,
    def forward(self, x):
        return x.view(*self.shape)

> 앞서 몇 가지 함수를 정의하는 코드이다.

generate_random_image 는 무작위 사진을 불러오는 함수이다.

generate_random_seed 는 무작위 시드를 불러오는 함수인데, G 생성기의 초기 이미지 시드 생성에 사용된다.

View 는 3차원 이미지 텐서를 1차원 이미지 텐서로 바꿔주는 함수이다.

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

    self.model = nn.Sequential(
        View(218*178*3),
        nn.Linear(218*178*3,100),
        nn.LeakyReLU(0.02),
        nn.LayerNorm(100),
        nn.Linear(100,1),
        nn.Sigmoid())
        
    self.loss_function = nn.BCELoss()
    self.optimiser = torch.optim.Adam(self.parameters(), lr = 0.01)
    self.counter = 0
    self.progress = []
    pass
  
  def forward(self, inputs):
    return self.model(inputs)

  def train(self,inputs,targets):
    outputs = self.forward(inputs)
    loss = self.loss_function(outputs,targets)
    self.counter +=1
    if(self.counter%10==0):
      self.progress.append(loss.item())
      pass
    if(self.counter%10000==0):
      print("counter = ",self.counter)
      pass
    
    self.optimiser.zero_grad()
    loss.backward()
    self.optimiser.step()
    pass
  
  def plot_progress(self):
    df = pandas.DataFrame(self.progress, columns=['loss'])
    df.plot(ylim=(0,1.0), figsize=(16,8), alpha=0.1, marker='.', grid=True, yticks=(0,0.25,0.5))
    pass

> Discriminator 판별기 함수 코드이다.

init 에서는 위의 MNIST와 비슷하지만, 3차원 이미지텐서를 사용하기에 View 함수와 함께 model을 구성해준다. model 구조는 MLP로 동일하다.

나머지도 MNIST 와 동일하며, 데이터 형식이 이미지 텐서로 바뀌었다는 점만 다르다.

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

    self.model = nn.Sequential(
        nn.Linear(100,3*10*10),
        nn.LeakyReLU(0.02),
        nn.LayerNorm(3*10*10),
        nn.Linear(3*10*10,3*218*178),
        nn.Sigmoid(),
        View((218,178,3)))

    self.optimiser = torch.optim.Adam(self.parameters(), lr = 0.01)
    self.counter = 0
    self.progress = []
    pass
  
  def forward(self, inputs):
    return self.model(inputs)

  def train(self,D,inputs,targets):
    g_output = self.forward(inputs)
    d_output = D.forward(g_output)
    loss = D.loss_function(d_output,targets)
    self.counter +=1
    if(self.counter%10==0):
      self.progress.append(loss.item())
      pass
    
    self.optimiser.zero_grad()
    loss.backward()
    self.optimiser.step()
    pass
    
  def plot_progress(self):
    df = pandas.DataFrame(self.progress, columns=['loss'])
    df.plot(ylim=(0,1.0), figsize=(16,8), alpha=0.1, marker='.', grid=True, yticks=(0,0.25,0.5))
    pass

> Generator 생성기 함수 코드이다.

마찬가지로 이미지 텐서에 대해 model 부분을 제외하고 MNIST와 동일하다.

D = Discriminator()
D.to(device)
G = Generator()
G.to(device)

epochs = 20
for i in range(epochs):
  for image_data_tensor in celeba_dataset:
    # 1단계
    D.train(image_data_tensor, torch.cuda.FloatTensor([1.0]))
    # 2단계
    D.train(G.forward(generate_random_seed(100)).detach(), torch.cuda.FloatTensor([0.0]))
    # 3단계
    G.train(D, generate_random_seed(100), torch.cuda.FloatTensor([1.0]))

> 모델 훈련을 위한 코드이다.

MNIST와 동일하다.

#1
D.plot_progress()

#2
'''
G.plot_progress()
'''

#3
'''
f, axarr = plt.subplots(2,3,figsize=(16,8))
for i in range(2):
  for j in range(3):
    output = G.forward(generate_random_seed(100))
    img = output.detach().cpu().numpy()
    axarr[i,j].imshow(img, interpolation='none', cmap='Blues')
    pass
  pass
'''

> 모델 훈련 결과 확인을 위한 코드이다.

MNIST와 동일하다.

물론 결과 확인은 주석을 하나씩 풀고, 다시 주석처리를 하면서 확인해보라. 아니면 동시에 실행시킬 경우 코드가 꼬이는 경우가 있다.

 

결론적으로 마찬가지로 데이터셋은 주어졌지만, 원하는 결과는 결국 '실제와 같은 이미지' 이므로 위 결과 코드를 통해 결과 이미지를 이용해볼 수 있다.

직접 제작한 데이터셋 (이미지 데이터 형식)을 활용한 GAN

더보기

1. 자체 제작 데이터셋을 만들어보자.

직접 그릴수도 있지만, 연구실에서 사용하는 PCB 기판 이미지를 사용하였다.

이미지 복원을 목표로 시작한 모델 학습이었기에 여러분은 원하는 이미지 아무거나 사용해도 상관없다.

연구로 사용된 이미지이므로 기록을 남길수는 없고 자체 그림판으로 그린 이미지로 대체하여 소개하고자 한다.

 

2. 이미지 구성은 다음과 같다.

PCB 소자에 대한 이미지 폴더와 그 속에 이미지들을 넣었다.

이러한 폴더가 하나 있고,

그 속에 이런식으로 생긴 PCB 이미지들이 제각각 존재한다.

 

3. CelebA 이미지 데이터셋을 훈련시켰던 것처럼 zip 으로 압축하고 경로만 바꾸어서 돌리면 된다.

 

4. 커널 초기화하면 다 사라지지 않는가. 당연하다 그래서 추가적으로 모델을 저장하고 불러오는 것까지 다루겠다.

(데이터 전처리를 다루려고 했으나 여기선 순수 GAN에 대해서만 다룰 예정이다.)

위 방법으로 훈련을 전부 마치고 결과 이미지까지 뽑았다고 가정하자.

 

torch.save(G.state_dict(), "./G.pt")
torch.save(D.state_dict(), "./D.pt")

> 모델을 저장하는 코드이다.

원하는 경로를 통해 G에 대한 모델을 G.pt (형식은 파라미터값을 저장하는 것이니 반드시 pt), D.pt 로 저장해주면 파일이 생성된다.

D = Discriminator()
G = Generator()
G.load_state_dict(torch.load("./G.pt"))
D.load_state_dict(torch.load("./D.pt"))
D.to(device)
G.to(device)

> 모델을 불러오는 코드이다.

모델을 불러와 다시 학습 시키기 위해서는 저장했던 경로를 불러와 파라미터값만 불러와 다시 학습이 가능하다. 모델 전체를 저장할수도 있으니 참고하길 바란다.

 

폴더에 이미지가 담긴 직접 만든 데이터셋으로 학습이 가능하며, 결과를 저장하고 불러와 지속적인 학습이 가능하다.


else

본 글에서는 GAN에 대한 기본적인 실습과 동시에 자체 데이터에 대한 학습이 가능하였다.

이후 글에서는 모델 구조 변경 및 최적화를 위한 파라미터 변경을 다룰 예정이다.

이 글을 통해 일단 아무 이미지 폴더만 가져와서 바로 훈련해보고 결과를 뽑을 수 있었으면 하는 바램에서 이 글을 마친다.