본문 바로가기
  • overfitting AI , overfitting deep learning
컴퓨터비전

캐릭터 생성모델 프로젝트

by J.I SHIN 2022. 12. 25.

코드스테이츠에서 진행한 AI 부트캠프 7개월간 다양한 프로젝트를 진행했습니다.

캐릭터 생성모델 프로젝트는 다섯 번째로 진행한 프로젝트로 2주가 소요되었습니다.

 

 

1. 문제 정의

"이게 가능할까?" 라는 의문에서 시작된 프로젝트

 

당시 큰 관심을 가졌던 생성 모델을 주제로 선정하게 되었고,

그 중에서 pix2pix 그리고 cycleGAN이라는 생성 모델을 활용하여

스케치를 채색하는 인공지능 모델을 만들어 보고자 하였습니다.

 

 

Miyazaki Hayao Characters / 출처 : film daily

 

지브리 애니메이션을 선택한 이유가 몇 가지 있습니다.

캐릭터의 색채나 그림체에 어느정도 일관성이 있으며

많은 작품이 나온만큼 많은 양의 캐릭터 데이터를 확보할 수 있었습니다.

 

아래는 프로젝트 진행 내용에 관한 것입니다.

 

 

2. 데이터 준비

지브리 스튜디오 애니메이션 스타일 채색 모델을 만들고자

캐릭터의 얼굴을 직접 캡처하고, 이를 스케치화 한 데이터셋을 만들었습니다.

이 과정에서 많은 시간을 소비하였습니다.

 

캐릭터 채색 인공지능을 만들기 위해 다음과 같이

풀컬러 + 스케치 이미지 데이터 쌍을 만들었습니다.

 

스케치 이미지 제작의 경우 가우시안 필터 등을 사용할 수 도 있었지만

무료로 배포된 공개 프로그램의 성능이 괜찮아 이를 활용하였습니다.

 

 

 

GitHub - qhgz2013/anime-face-detector: A Faster-RCNN based anime face detector implementation using tensorflow.

A Faster-RCNN based anime face detector implementation using tensorflow. - GitHub - qhgz2013/anime-face-detector: A Faster-RCNN based anime face detector implementation using tensorflow.

github.com

물론 캐릭터 얼굴 데이터를 보다 쉽게 확보할 수도 있었습니다.

위와 같은 캐릭터의 얼굴을 찾는 모델을 활용하는 방법과 같이 말이죠.

 

하지만 'Garbage in, Gargbage out.'

좋은 데이터를 넣어주는게 좋다고 판단하였습니다.

 

정제된 데이터를 사용하기 위해 직접 crop 하였습니다.

정사각형으로 자르기를 진행하였고, 이후 가로 세로 픽셀을 통일해주었습니다.

 

많은 데이터를 쓰고 싶었지만 시간이 부족했고, 모델을 만드는 것에 의미를 두고

500장+500장의 이미지만으로 모델을 학습시켰습니다.

 

 

3. 모델 훈련

 

cycleGAN 모델을 훈련하는 코드입니다.

다양한 예시가 있지만 Tensorflow를 활용한 패키지를 불러옵니다.

!pip install tensorflow_addons

from __future__ import print_function, division
import scipy
from tensorflow.keras.datasets import mnist
from tensorflow_addons.layers import InstanceNormalization
from tensorflow.keras.layers import Input, Dense, Reshape, Flatten, Dropout, Concatenate
from tensorflow.keras.layers import BatchNormalization, Activation, ZeroPadding2D
from tensorflow.keras.layers import LeakyReLU
from tensorflow.keras.layers import UpSampling2D, Conv2D
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.optimizers import Adam
import datetime
import matplotlib.pyplot as plt
import sys
import numpy as np
import os

 

 

CycleGAN 클래스를 만들고 매개변수와 데이터 로더를 초기화합니다.

class CycleGAN():
    def __init__(self):
        # 입력 크기
        self.img_rows = 128
        self.img_cols = 128
        self.channels = 3
        self.img_shape = (self.img_rows, self.img_cols, self.channels)

        # 데이터 로더 설정
        self.dataset_name = 'apple2orange'
        # DataLoader 객체를 사용해 전처리된 데이터 임포트합니다.
        self.data_loader = DataLoader(dataset_name=self.dataset_name,
                                      img_res=(self.img_rows, self.img_cols))

        # D(PatchGAN)의 출력 크기를 계산합니다.
        patch = int(self.img_rows / 2**4)
        self.disc_patch = (patch, patch, 1)

        # G와 D의 첫 번째 층에 있는 필터의 개수
        self.gf = 32
        self.df = 64

        # 손실 가중치
        self.lambda_cycle = 10.0                    # 사이클-일관성 손실 가중치
        self.lambda_id = 0.9 * self.lambda_cycle    # 동일성 손실 가중치

        optimizer = Adam(0.0002, 0.5)
        
        #######################################################################
        
        # 판별자 A와 B를 만들고 컴파일합니다.
        self.d_A = self.build_discriminator()
        self.d_B = self.build_discriminator()
        self.d_A.compile(loss='mse',
                         optimizer=optimizer,
                         metrics=['accuracy'])
        self.d_B.compile(loss='mse',
                         optimizer=optimizer,
                         metrics=['accuracy'])

        #-------------------------
        # 생성자의 계산 그래프를 만듭니다.
        #-------------------------

        # 생성자 AB, BA를 만듭니다.
        self.g_AB = self.build_generator()
        self.g_BA = self.build_generator()

        # 두 도메인의 입력 이미지
        img_A = Input(shape=self.img_shape)
        img_B = Input(shape=self.img_shape)

        # 이미지를 다른 도메인으로 변환합니다.
        fake_B = self.g_AB(img_A)
        fake_A = self.g_BA(img_B)
        # 원본 도메인으로 이미지를 다시 변환합니다.
        reconstr_A = self.g_BA(fake_B)
        reconstr_B = self.g_AB(fake_A)
        # 동일한 이미지 매핑
        img_A_id = self.g_BA(img_A)
        img_B_id = self.g_AB(img_B)

        # 연결 모델에서는 생성자만 훈련합니다.
        self.d_A.trainable = False
        self.d_B.trainable = False

        # 판별자가 변환된 이미지를 검증합니다.
        valid_A = self.d_A(fake_A)
        valid_B = self.d_B(fake_B)

        # 연결 모델은 판별자를 속이기 위한 생성자를 훈련합니다.
        self.combined = Model(inputs=[img_A, img_B],
                              outputs=[valid_A, valid_B,
                                       reconstr_A, reconstr_B,
                                       img_A_id, img_B_id])
        self.combined.compile(loss=['mse', 'mse',
                                    'mae', 'mae',	# 유효성, 재구성, 동일성 손실
                                    'mae', 'mae'],	#  A-B-A와 B-A-B 총 6개
                              loss_weights=[1, 1,
                                            self.lambda_cycle, self.lambda_cycle,
                                            self.lambda_id, self.lambda_id],
                              optimizer=optimizer)

중간쯤 lambda_cycle, lambda_id 를 볼 수 있는데

이 두 값은 CycleGAN에서 중요한 파라미터입니다.

 

lambda_cycle은 사이클 일관성 손실을 얼마나 엄격하게 강제할지 제어하는 값으로

이 값을 높게 설정하면 원본 이미지와 재구성 이미지가 가능한 한 가까워진다고 합니다.

 

lambda_id값에 대해 CycleGAN의 저자는 변화에 극적으로 영향을 미치는 값이라 언급하는데

이 값이 낮으면 불필요한 변화가 생길 수 있다고 합니다. (색상 반전 등)

훈련을 여러번 진행하여 자신의 데이터셋에 맞는 값을 찾는 과정이 필요해 보입니다.

 

계속해서 두 개의 판별자와 두 개의 생성자를 만드는 코드가 이어집니다.

여기서 6개의 loss를 사용하게 되는데, 이는 유효성, 재구성, 동일성 손실입니다.

A-B-A에서 3개, B-A-B에서 3개이기 때문에 6개의 loss를 가지며, 처음 손실만 제곱 오차를 사용합니다.

 

 

 

다운샘플링과 업샘플링 층을 설정합니다.

class CycleGAN(CycleGAN):
      @staticmethod
      def conv2d(layer_input, filters, f_size=4, normalization=True):
        """다운샘플링하는 동안 사용되는 층"""
        d = Conv2D(filters, kernel_size=f_size,
                   strides=2, padding='same')(layer_input)
        d = LeakyReLU(alpha=0.2)(d)
        if normalization:
            d = InstanceNormalization()(d)
        return d
      
      @staticmethod
      def deconv2d(layer_input, skip_input, filters, f_size=4, dropout_rate=0):
            """업샘플링하는 동안 사용되는 층"""
            u = UpSampling2D(size=2)(layer_input)
            u = Conv2D(filters, kernel_size=f_size, strides=1,
                       padding='same', activation='relu')(u)
            if dropout_rate:
                u = Dropout(dropout_rate)(u)
            u = InstanceNormalization()(u)
            u = Concatenate()([u, skip_input])
            return u

 

 

생성자를 만드는 부분입니다. 여기서 비교적 구현이 쉬운 U-net 구조를 사용합니다.

class CycleGAN(CycleGAN):
    def build_generator(self):
        """U-Net 생성자"""
        # 이미지 입력
        d0 = Input(shape=self.img_shape)

        # 다운샘플링
        d1 = self.conv2d(d0, self.gf)
        d2 = self.conv2d(d1, self.gf * 2)
        d3 = self.conv2d(d2, self.gf * 4)
        d4 = self.conv2d(d3, self.gf * 8)

        # 업샘플링
        u1 = self.deconv2d(d4, d3, self.gf * 4)
        u2 = self.deconv2d(u1, d2, self.gf * 2)
        u3 = self.deconv2d(u2, d1, self.gf)

        u4 = UpSampling2D(size=2)(u3)
        output_img = Conv2D(self.channels, kernel_size=4,
                            strides=1, padding='same', activation='tanh')(u4)

        return Model(d0, output_img)

 

 

판별자를 만드는 부분입니다. 

class CycleGAN(CycleGAN):
    def build_discriminator(self):
      img = Input(shape=self.img_shape)

      d1 = self.conv2d(img, self.df, normalization=False)
      d2 = self.conv2d(d1, self.df * 2)
      d3 = self.conv2d(d2, self.df * 4)
      d4 = self.conv2d(d3, self.df * 8)

      validity = Conv2D(1, kernel_size=4, strides=1, padding='same')(d4)

      return Model(img, validity)

 

 

cycleGAN 훈련 알고리즘을 구현한 코드입니다.

class CycleGAN(CycleGAN):
      def sample_images(self, epoch, batch_i):
        r, c = 2, 3

        imgs_A = self.data_loader.load_data(domain="A", batch_size=1, is_testing=True)
        imgs_B = self.data_loader.load_data(domain="B", batch_size=1, is_testing=True)
        
        # 이미지를 다른 도메인으로 변환합니다.
        fake_B = self.g_AB.predict(imgs_A)
        fake_A = self.g_BA.predict(imgs_B)
        # 원본 도메인으로 되돌립니다.
        reconstr_A = self.g_BA.predict(fake_B)
        reconstr_B = self.g_AB.predict(fake_A)

        gen_imgs = np.concatenate([imgs_A, fake_B, reconstr_A, imgs_B, fake_A, reconstr_B])

        # 이미지를 0 - 1 사이로 스케일을 바꿉니다.
        gen_imgs = 0.5 * gen_imgs + 0.5

        titles = ['Original', 'Translated', 'Reconstructed']
        fig, axs = plt.subplots(r, c)
        cnt = 0
        for i in range(r):
            for j in range(c):
                axs[i,j].imshow(gen_imgs[cnt])
                axs[i, j].set_title(titles[j])
                axs[i,j].axis('off')
                cnt += 1
        fig.savefig("images/%s/%d_%d.png" % (self.dataset_name, epoch, batch_i))
        plt.show()

 

 

 

최종적으로 훈련을 얼만큼 할지 결정하고 시작하기 위한 위한 코드입니다.

cycle_gan = CycleGAN()
cycle_gan.train(epochs=150, batch_size=64, sample_interval=10)

 

 

4. 결과

 

컬러 이미지를 스케치 이미지로 만드는 성능은 비교적 잘 나왔지만,

스케치 이미지를 채색하는 성능은 아직 많이 부족한 것 같습니다.

 

그래도 얼굴과 머리, 옷, 배경을  조금이나마 구분해서 색칠하는 것 같습니다.

GAN의 대표적인 문제인 blur 현상도 많이 나타났습니다.

 

 

 

5. 성능 평가

 

 

두 가지 클래스의 이미지를 학습시켜 생성모델이 채색을 할 수 있는지 확인하고

IS(Inception Score)와 FID(Frechet Inception Distance)를 활용해

정량적인 평가도 함께 진행하였습니다.

 

FID를 계산하는 코드는 아래와 같습니다.

# calculate frechet inception distance

def calculate_fid(model, images1, images2):
	# calculate activations
	act1 = model.predict(images1)
	act2 = model.predict(images2)
	# calculate mean and covariance statistics
	mu1, sigma1 = act1.mean(axis=0), cov(act1, rowvar=False)
	mu2, sigma2 = act2.mean(axis=0), cov(act2, rowvar=False)
	# calculate sum squared difference between means
	ssdiff = numpy.sum((mu1 - mu2)**2.0)
	# calculate sqrt of product between cov
	covmean = sqrtm(sigma1.dot(sigma2))
	# check and correct imaginary numbers from sqrt
	if iscomplexobj(covmean):
		covmean = covmean.real
	# calculate score
	fid = ssdiff + trace(sigma1 + sigma2 - 2.0 * covmean)
	return fid

 

 

비교적 잘 만들어진 이미지를 사용한 결과 FID 값이 40~50 정도가 나왔습니다.

FID값은 0에 가까울 수록 좋으며, 10 내외의 값이 좋은 수치라고 할 수 있습니다.

 

비록 좋은 수치는 아니지만, 적은 데이터로 짧게 훈련한 것 치고는 잘 나왔다고 판단됩니다.

모델을 개선하고 훈련을 늘린 후 어떠한 FID가 나올지 추가적으로 실험해 볼 계획입니다.

 

 

 

 

프로젝트 코드는 아래 깃허브에서 확인하실 수 있습니다.

 

GitHub - gitteor/GAN_Project

Contribute to gitteor/GAN_Project development by creating an account on GitHub.

github.com

 

 

감사합니다. : )

댓글