from IPython.core.display import display, HTML
display(HTML("<style>.container { width:90% !important; }</style>"))
⛳ 새로운 패션 만들기 코드로 살펴보자 ⛳¶
3-2. 코드로 살펴보자.¶
사용할 데이터셋이 어떤 것인지 알았으니, 코드를 실행해보자.
필요한 패키지는 신경망 구성에 필요한 Tensorflow, 이미지와 GIF를 다루는 imageio, display, matplotlib, PIL 등 이 필요하다.
import os
import glob
import time
import PIL
import imageio
import numpy as np
import tensorflow as tf
from tensorflow.keras import layers
from IPython import display
import matplotlib.pyplot as plt
%matplotlib inline
print('tensorflow', tf.__version__)
tensorflow 2.7.0
필요한 패키지를 실행시켰으니, 데이터셋을 가져와보자.
fashion_mnist 데이터는 tf.keras 안에 있는 datasets 에 이미 들어있어서 꺼내어 사용하면 된다.
load_data()
로 데이터를 로딩할 것인데, 이번에는 분류 문제와 달리,
각 이미지가 어떤 카테고리인지 나타내주는 label이 필요 없다.
즉, MNIST 데이터로 분류 문제를 풀었을 때 필요한 y_train
, y_test
에 해당하는 데이터는 사용하지 않는다는 것이다.
그래서 코드에서 ' _ ' (언더스코어) 로 해당 데이터는 무시하도록 한다.
fashion_mnist = tf.keras.datasets.fashion_mnist
(X_train, _), (X_test, _) = fashion_mnist.load_data()
⏳ Fashion MNIST 또한 MNIST 와 같이 28 X 28 픽셀의 이미지로, 각 픽셀은 0 ~ 255 사이의 정수값을 가진다.
print('Max pixel:', X_train.max())
print('Min pixel:', X_train.min())
Max pixel: 255 Min pixel: 0
⏳ 이번 프로젝트에서는 각 픽셀을 -1, 1 로 정규화시켜 사용할 것이여서 중간값을 0 으로 맞추기 위해
127.5를 뺀 후 127.5 로 나누어줄 것이다.
# 이미지를 [-1, 1] 로 정규화하기 위한 작업
X_train = (X_train - 127.5) / 127.5
print('Max pixel:', X_train.max())
print('Min pixel:', X_train.min())
Max pixel: 1.0 Min pixel: -1.0
⏳ 데이터셋의 shape 은 어떨까?
X_train.shape
(60000, 28, 28)
⏳ train 데이터셋에는 6만장의 이미지와 사이즈는 28 X 28 인 것을 확인할 수 있다.
⏳ 다만 한 가지 더 추가되야 할 부분이 있는데,
CNN(합성곱) 계층을 다룰 때, 딥러닝에서 이미지를 다루려면 채널 수 에 대한 차원이 필요하다.
컬러의 경우 R, G, B 세 개의 채널이 있고, Gray Scale(흑백) 의 경우 1개의 채널이 필요하다.
Fashion MNIST 의 경우 데이터는 흑백이므로, 채널 값은 1을 가지게 되어 데이터셋의 shape 마지막에 1 을 추가해야 된다.
X_train = X_train.reshape(X_train.shape[0], 28, 28, 1).astype('float32')
X_train.shape
(60000, 28, 28, 1)
⏳ reshape()
를 통해 마지막에 1의 채널이 추가된 것을 볼 수 있다.
⏳ 첫 번째 데이터를 꺼내어 어떻게 생겼는지 확인해보자.
plt.imshow(X_train[0].reshape(28, 28), cmap='gray')
plt.colorbar()
plt.show()
⏳ plt.colorbar()
을 이용해 오른쪽에 각 픽셀의 값과 그에 따른 색이 확인이 가능하다.
픽셀에는 정규화 해준 [-1, 1] 사이의 값을 가지고, -1 이 가장 어두운 검은색, 1 이 가장 밝은 흰색을 표시한다.
⏳ for 문을 이용해 열 개 정도 띄어보자.
plt.figure(figsize=(10, 5))
for i in range(10):
plt.subplot(2, 5, i + 1) # 여러개 이미지 띄우기
plt.imshow(X_train[i].reshape(28, 28), cmap='gray')
plt.title(f'index: {i}')
plt.axis('off')
plt.show()
⏳ 여러개의 이미지를 2 X 5의 배열로 띄울려면, plt.subplot(row, col, index)
의 형태로 만든다.
⏳ for 문을 이용해서 25개의 5 X 5 배열로 train_images
에서 랜덤으로 사진을 추출해보자.
plt.figure(figsize=(10, 12))
for i in range(25):
plt.subplot(5, 5, i + 1)
random_index = np.random.randint(11, 60000)
plt.imshow(X_train[random_index].reshape(28, 28), cmap='gray')
plt.title(f'index: {random_index}')
plt.axis('off')
plt.show()
⏳ 이제 대이터 전처리는 모두 끝이났다.
⏳ 정리된 데이터를 모델에 넣어 학습을 시켜야되니, 편하게 쓸수 있도록 Tensorflow 의 Dataset 을 이용해 준비하자.
이를 이용하면 매번 모델에 직접 섞어 넣어주지 않아도 된다.
BUFFER_SIZE = 60000
BATCH_SIZE = 256
⏳ BUFFER_SIZE 는 전체 데이터를 섞기 위해 60,000 으로 설정한다.
⏳ shuffle()
함수가 데이터셋을 잘 섞어서 모델에 넣어줄 것이다.
⏳ BATCH_SIZE 는 모델이 한 번에 학습할 데이터 양이다.
너무 많은 양을 한 번에 학습시키면 메모리 활용면에서 비효율적이고,
한 번 학습을 하는 데에도 오랜 시간이 걸려 적절한 사이즈로 잘라서 학습을 진행하는 것이 좋다.
이런 방식을 미니 배치 학습 이라 한다.
train_dataset = tf.data.Dataset.from_tensor_slices(X_train).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
train_dataset
<BatchDataset shapes: (None, 28, 28, 1), types: tf.float32>
⏳ tf.data.Dataset 모듈의 from_tensor_slices()
함수를 사용하면
리스트, 넘파이, 또는 텐서플로우의 텐서 자료형에서 데이터셋을 만들 수 있다.
⏳ 위 코드는 X_train
라는 넘파이 배열(numpy ndarray) 형 자료를 섞고, 이를 배치 사이즈에 따라 나눈 것이다.
데이터가 잘 섞이게 하기 위해서는 버퍼 사이즈를 총 데이터 사이즈와 같거나 크게 설정하는 것이 좋다.
4. 그림을 만들어내는 화가 생성자, 평가하는 비평가 구분자¶
4-1. GAN 이해하기¶
GAN 이란??
GAN 은 2014년 Lan Goodfellow 라는 연구자에 의해 처음 나온 네트워크로,
발표 이후 독특하면서 직관적인 아이디어로 많은 인기를 얻고 지금까지고 굉장히 많은 연구가 이루어지고 있는 모델이다.
GAN 은 생성 모델 중 하나이다.
초반부 DeepComposer 형상에서 설명했듯, GAN 에는 두 가지 네트워크가 있다.
생성자(Generator): 아무 의미 없는 랜덤 노이즈로부터 신경망에서 연산을 통해 이미지 형상의 벡터를 생성
무에서 유를 창고하는 것고 같은 역할판별자(Discriminator): 기존에 있던 진짜 이미지와 생성자가 만들어낸 이미지를 입력받아
각 이미지가 Real 인지, Fake 인지에 대한 판단 정도를 실수값으로 출력
Lan Goodfellow 가 GAN을 처음 발표했던 2014년의 논문에서는 이 두 네트워크를 다음과 같이 비교하였다.
"In the proposed adversarial nets framework, the generative model is pitted against an adversary: a discriminative model that learns to determine whether a sample is from the model distribution or the data distribution. The generative model can be thought of as analogous to a team of counterfeiters, trying to produce fake currency and use it without detection, while the discriminative model is analogous to the police, trying to detect the counterfeit currency. Competition in this game drives both teams to improve their methods until the counterfeits are indistiguishable from the genuine articles."
Generative Adversarial Nets
위 글에서
Generative Model : 위조지폐를 만들어내는 팀
Discriminative Model : 위조 지폐범을 잡아내는 경찰
즉, 위조지폐를 만들어내는 팀은 잡히지 않기 위해 더 진짜 같은 지폐를 만들려고 하며, 그것을 잡아내려는 경찰은 끊임없이 진짜 지폐와 위조지폐를 구분하려고 한다. 즉, 서로 간의 경쟁이 둘 모두를 성장하게 한다. 궁극적인 목표는 진짜 지폐와 구분될 수 없는 위조지폐를 만들어 내는 것이다.
🔥 이렇게 서로 경쟁하듯 이루어진 모델의 구조 덕분에 이름에 "Adversarial(적대적인)" 이 들어가게 된 것이다.(Generative Adversarial Nets)
GAN 중 특히 합성곱 층(Convolutional Layer)으로 이루어진 딥러닝 모델을 DCGAN (Deep Convolutional GAN) 이라고 합니다. 본 글에서 설명한 DCGAN은 무엇을 입력받아 무엇을 출력하나?
- Input : 100 random numbers drawn from a uniform distribution (called as a code, or latent variables) - Output : an image (in this case 64x64x3 images)
글에서는 GAN을 포함해 총 세 가지의 생성 모델링 기법을 소개하였습니다. 나머지 두 가지는 무엇인가?
- Variational Autoencoders (VAEs) - Autoregressive models (ex. Pixel RNN)
4-2. 생성자 구현하기¶
GAN 의 모델 구조를 알아보았으니, 코드를 통해 모델을 어떻게 구현할 수 있는지 알아보자.
GAN 에는 생성자, 판별자 모델 두 개가 있다
우리가 구현해 볼 모델은 바로 위에서 언급했던 DCGAN(Deep ConvolutionalGAN) 이다.
DCGAN 은 GAN 의 개념이 처음 소개된 1년 반 이후 발표된 논문으로 이전의 GAN을 발전시켜
고화질의 이미지 생성을 이루어낸 첫 번째 논문으로 평가받고 있다.
이후 발표된 수 많은 GAN 기반 이미 생성 모델들은 대부분 DCGAN 모델을 발전시킨 형태이다.
모델의 구현은 Keras Sequential
API 를 활용할 것이다.
이후 소개하는 코드는 Tensorflow 2.0 Tutorial 의 DCGAN 구현을 기반으로 상세한 설명을 추가하였다.
def make_generator_model():
# Start
model = tf.keras.Sequential()
# First: Dense layer
model.add(layers.Dense(7*7*256, use_bias=False, input_shape=(100,)))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
# Second: Reshape layer
model.add(layers.Reshape((7, 7, 256)))
# Third: Conv2DTranspose layer
model.add(layers.Conv2DTranspose(128, kernel_size=(5, 5), strides=(1, 1), padding='same', use_bias=False))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
# Fourth: Conv2DTranspose layer
model.add(layers.Conv2DTranspose(64, kernel_size=(5, 5), strides=(2, 2), padding='same', use_bias=False))
model.add(layers.BatchNormalization())
model.add(layers.LeakyReLU())
# Fifth: Conv2DTranspose layer
model.add(layers.Conv2DTranspose(1, kernel_size=(5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh'))
return model
⏳ make_generator_model
함수를 만들어 언제든 생성자를 생성가능하게 만들었다.
⏳ 함수 내부에서는 먼저 tf.keras.Sequential()
로 모델을 시작한 후 레이어를 차곡차곡 쌓아준다.
⏳ 가장 중요한 레이어는 Conv2DTranspose
레이어 이다.
Con2DTranspose
층은 일반적인 Conv2D
와 반대로 이미지 사이즈를 넓혀주는 층이다.
이 모델에서는 세 번의 Conv2DTranspose
층을 이용해 (7, 7, 256) → (14, 14, 64) → (28, 28, 1)
순으로 이미지를 키워나간다.
⏳ 최종 사이즈는 (28, 28, 1)로 우리가 준비했던 데이터셋과 형상이 동일하다.
⏳ 레이어의 사이사이에 특정 층들이 반복되는 것을 볼 수 있다.
BatchNormalization
레이어는 신경망의 가중치가 폭발하지 않도록 가중치 값을 정규화 시켜준다.
또한 중간층들의 활성화 함수는 모두 LeakyReLU
를 사용하였으며,
마지막 층에는 tanh
(하이퍼볼릭탄젠트 함수로 시그모이드 함수를 transformation해서 얻을 수 있다.)를 사용하는데,
이유는 (-1 ~ 1) 이내의 값으로 픽셀 값을 정규화시켰던 데이터셋과 동일하게 하기 위함이다.
생성자의 입력 벡터는 어떤 형태인가?
- 모델은 입력값으로 (batc_size, 100) 형상의 노이즈 벡터를 받는다.
처음 입력된 벡터는 어떤 레이어를 지나며, 첫 레이어를 지난 후의 shape는 어떤 형태인가?
- 입력된 (batch_size, 100) 벡터는 7 X 7 X 256 = 12544 개의 노드를 가진 첫 번째 Dense 레이어를 거치며 (batch_size, 12544) 형태의 벡터가 된다.
첫 번째 레이어를 지난 후 벡터는 어떤 층을 지나게 되나?? 이 레이어는 왜 필요한가??
- 첫 번째 레이어를 지난 후 벡터는 Reshape 레이어를 지나게 된다. - 이는 이후의 layer 에서 Convolutional 연산을 할 수 있도록 1차원 벡터를 (7, 7, 256) 형상의 3차원 벡터로 변환시키는 작업이다.
⏳ 생성 모델을 generator
변수로 생성하고, 모델 세부 내용인 summary 를 출력해보자.
generator = make_generator_model()
generator.summary()
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense (Dense) (None, 12544) 1254400 _________________________________________________________________ batch_normalization (BatchNo (None, 12544) 50176 _________________________________________________________________ leaky_re_lu (LeakyReLU) (None, 12544) 0 _________________________________________________________________ reshape (Reshape) (None, 7, 7, 256) 0 _________________________________________________________________ conv2d_transpose (Conv2DTran (None, 7, 7, 128) 819200 _________________________________________________________________ batch_normalization_1 (Batch (None, 7, 7, 128) 512 _________________________________________________________________ leaky_re_lu_1 (LeakyReLU) (None, 7, 7, 128) 0 _________________________________________________________________ conv2d_transpose_1 (Conv2DTr (None, 14, 14, 64) 204800 _________________________________________________________________ batch_normalization_2 (Batch (None, 14, 14, 64) 256 _________________________________________________________________ leaky_re_lu_2 (LeakyReLU) (None, 14, 14, 64) 0 _________________________________________________________________ conv2d_transpose_2 (Conv2DTr (None, 28, 28, 1) 1600 ================================================================= Total params: 2,330,944 Trainable params: 2,305,472 Non-trainable params: 25,472 _________________________________________________________________
⏳ 모델이 만들어졌으니, shape=(1, 100)
의 형상을 가지는 랜덤 노이즈 벡터를 생성해서 결과물을 만들어보자.
⏳ tf.random.normal
을 이용하면 가우시안 분포에서 뽑아낸 랜덤 벡터로 이루어진 노이즈 벡터를 만들 수 있다.
noise = tf.random.normal([1, 100])
noise
<tf.Tensor: shape=(1, 100), dtype=float32, numpy= array([[ 5.41887879e-01, 1.11103463e+00, -1.29451048e+00, -1.05398223e-01, 7.74308369e-02, 1.52680755e+00, 1.14797008e+00, 1.12994850e-01, -9.69035387e-01, -9.12803039e-03, 2.06668091e+00, 5.06269395e-01, 9.20731246e-01, 1.86704218e-01, -1.80583704e+00, -1.33466860e-03, 6.60861954e-02, 4.31053966e-01, 1.27198279e+00, 5.81985176e-01, 1.30361128e+00, 1.74059331e-01, -5.22588313e-01, 1.00485015e+00, -1.20496703e-02, -1.86255586e+00, -1.36782455e+00, -1.34617674e+00, 9.94900525e-01, 3.15389633e-01, 1.61300171e-02, 5.84501684e-01, -7.83775866e-01, -1.02413130e+00, -1.46220982e-01, -1.78035572e-01, 1.29716361e+00, 3.10411632e-01, -2.44246751e-01, -3.43516290e-01, 6.01105750e-01, -5.44264436e-01, 4.96103525e-01, 1.59957194e+00, 5.77038646e-01, 2.85712314e+00, 1.14819217e+00, 8.46932590e-01, -7.96359658e-01, 1.92748919e-01, 6.01148307e-01, -1.15959287e+00, 7.81422704e-02, -9.42251086e-01, -4.03169334e-01, -8.82573903e-01, -7.11332262e-01, 1.11857629e+00, -8.46708119e-01, 1.46012574e-01, -3.72442305e-01, 3.82315814e-01, 7.97113419e-01, -8.26116145e-01, 3.10476691e-01, -6.36935771e-01, -1.58271301e+00, 1.26831889e+00, 8.09921265e-01, 4.19315279e-01, -5.92232227e-01, -7.66745135e-02, 4.46917742e-01, -3.15382838e-01, 6.56181037e-01, -2.16873479e+00, -8.75261962e-01, 1.02059555e+00, -1.38336837e+00, 2.34190136e-01, 5.24857998e-01, 1.84012115e+00, -5.64899325e-01, 5.39173663e-01, 4.72241968e-01, -7.42459893e-01, 6.33423150e-01, -2.13316512e+00, -4.58842158e-01, 5.11019588e-01, 1.52409983e+00, 1.22117445e-01, 2.32623786e-01, 1.60076201e-01, -8.58774424e-01, 1.02735686e+00, 2.25035381e+00, -9.32674229e-01, 1.37605339e-01, 3.88507694e-01]], dtype=float32)>
⏳ Tensorflow 2.0 이후 버전에서는 레이어와 모델에 call 메소드를 구현해 놓기 때문에,
함수로 만들어 놓은 make_generator_model()
생성자 모델에 입력값으로 노이즈를 넣고
바로 모델을 호출하면 결과 이미지가 생성된다. (내부적으로 생성자의 call 함수가 호출)
⏳ 지금은 학습하는 중이 아니니 training=False
를 설정해주어야 한다.
Batch Normalization 레이어는 훈련 시기와 추론(infernce) 시기의 행동이 다르기 때문에
training=False
을 주어야 올바른 결과를 얻을 수 있다.
generated_image = generator(noise, training=False)
generated_image.shape
TensorShape([1, 28, 28, 1])
⏳ [1, 28, 28, 1]
사잊의 이미지가 생성이 되었다.
첫 번째 1은 1개(batch_size=1) 라는 뜻
이 후 (28, 28, 1) 사이즈 이미지가 생성되었다는 뜻
⏳ 이미지를 시각화 해보자.
matplotlib
라이브러리는 2차원 이미지만 볼 수 있으므로
0번째와 3번째 축의 인덱스를 0으로 설정해서 (28, 28)
shape 의 이미지를 꺼내도록 해보자.
plt.imshow(generated_image[0, :, :, 0], cmap='gray')
plt.colorbar()
plt.show()
⏳ -1 과 1 사이의 값에서 생성된 것을 확인 할 수 있다.
아직은 모델이 학습하기 전 상태여서 아무 의미가 없는 노이즈 같은 이미지가 생성되었지만,
학습을 하게 되면 제대로 된 이미지를 생성하게 될 것이다.
4-3. 판별자 구현하기¶
판별자는 가짜 이미지와 진짜 이미지를 입력받으면 각 이미지 별로 '진짜라고 판단되는 정도' 값을 출력한다.
입력은 (28, 28, 1) 크기의 이미지가, 출력은 단 하나의 실수값(진짜라고 판단하는 정도) 이다.
def make_discriminator_model():
# Start
model = tf.keras.Sequential()
# First: Conv2D Layer
model.add(layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[28, 28, 1]))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
# Second: Conv2D Layer
model.add(layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'))
model.add(layers.LeakyReLU())
model.add(layers.Dropout(0.3))
# Third: Flatten Layer
model.add(layers.Flatten())
# Fourth: Dense Layer
model.add(layers.Dense(1))
return model
⏳ 판별자 모델은 make_discriminator_model
함수로 구현하였다.
⏳ Conv2DTranspose
층을 사용해서 이미지를 키워나갔던 생성자와 반대로,
판별자는 Conv2D
층으로 이미지의 크기를 점점 줄여나간다.
첫 번째 Conv2D 층에서 입력된 [28, 28, 1]
사이즈의 이미지는 다음 층을 거치며
(28, 28, 1) → (14, 14, 64) → (7, 7, 128)
까지 줄어들게 된다.
⏳ 마지막 Flatten
층을 사용해 3차원 이미지를 1차원으로 펴서 7 X 7 X 128 = 6272,
즉 1,6272 형상의 베터로 변환한다. 이 생성자의 Reshape
층에서 1차원 벡터를 3차원으로 변환했던 것과 정확히 반대 역할을 한다.
1차원 벡터로 변환 후 마지막 Dense
layer 를 거쳐 단 하나의 값을 출력하게 된다.
⏳ 판별 모델 discriminator
라는 변수 이름으로 생성하고, 모델 세부 내용인 summary 를 출력해보자.
discriminator = make_discriminator_model()
discriminator.summary()
Model: "sequential_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= conv2d (Conv2D) (None, 14, 14, 64) 1664 _________________________________________________________________ leaky_re_lu_3 (LeakyReLU) (None, 14, 14, 64) 0 _________________________________________________________________ dropout (Dropout) (None, 14, 14, 64) 0 _________________________________________________________________ conv2d_1 (Conv2D) (None, 7, 7, 128) 204928 _________________________________________________________________ leaky_re_lu_4 (LeakyReLU) (None, 7, 7, 128) 0 _________________________________________________________________ dropout_1 (Dropout) (None, 7, 7, 128) 0 _________________________________________________________________ flatten (Flatten) (None, 6272) 0 _________________________________________________________________ dense_1 (Dense) (None, 1) 6273 ================================================================= Total params: 212,865 Trainable params: 212,865 Non-trainable params: 0 _________________________________________________________________
⏳ 위에서 생성했던 가짜 이미지를 판별자에 넣으면 어떤 결과가 나올까????
decision = discriminator(generated_image, training=False)
decision
<tf.Tensor: shape=(1, 1), dtype=float32, numpy=array([[0.00020918]], dtype=float32)>
⏳ Tensorflow의 Tensor 형태로 출력이 되었다. 이또 한 아직 의미없는 값인 것 같다.
5. 생성 모델이 smart 해지기 위한 방법¶
5-1. 손실함수와 최적화 함수¶
딥러닝 모델이 학습하기 위해서 꼭 필요한 두 가지는, 손실함수(loss function) 와 최적화 함수(optimizer) 이다.
손실함수(loss function)
GAN 은 손실함수로 교차 엔트로피(Cross Entropy)를 사용한다.
교차 엔트롶는 점점 가까워지기 원하는 두 값이 얼마나 큰 차이가 나는지를 정량적응로 계산할 때 많이 쓰인다.
특히 판별자는 한 개의 이미지가 가까인지 진짜인지 나타내는 2개 클래스 간 분류 문제를 풀어야 하므로,
이진 교차 엔트로피(binary corss entropy) 를 사용할 것이다.
Real Image 에 대한 label 을
1
Fake Image 에 대한 label 을
0
각각의 손실함수를 이용해 정량적으로 달성해야하는 결과는 아래와 같을 것이다.
생성자: 판별자가 Fake Image 에 대해 판별한 값, 즉
D(fake_image)
값이1
에 가까워지는 것판별자:
- Real Image 판별값, 즉
D(real_image)
는1
에, - Fake Image 판별값, 즉
D(fake_image)
는0
에 가까워지는 것
- Real Image 판별값, 즉
결국 생성자는 판별자든, 결국 손실함수에 들어가는 값은 모두 판별자의 판별값이 될 것이다.
손실함수에 사용할 교차 엔트로피 함수는 tf.keras.losses
라이브러리 안에 있다.
다만, 교차 엔트로피를 계산하기 위해 입력할 값은 판별자가 판별한 값인데,
판별자 모델의 마지막 Layer 에는 값을 정규화시키는 sigmoid 나 tanh 함수와 같은 활성화 함수가 없다.
판별자가 출력하는 값은 범위가 정해지지 않아 모든 실수값을 가질 수 있다.
그런데 tf.keras.losses 의 BinaryCrossEntropy 클래스는 기본적으로 본인에게 들어오는 인풋값이
0 - 1 사이에 분포하는 확률값이라 가정한다(참고). 따라서 from_logits
를 True
로 설정해 줘야 BinaryCrossEntropy
에 입력된 값을
함수 내부에서 sigmoid 함수를 사용해 0 ~ 1 사이의 값으로 정규화 한 후 계산할 수 있다.
cross_entropy = tf.keras.losses.BinaryCrossentropy(from_logits=True)
⏳ cross_entropy
를 활요애 계산할 loss 들은 fake_output
와 real_output
, 두 가지르 활용한다.
fake_output
: 생성자가 생성한 Fake Image 를 구분자에 입력시켜 판별된 값, 즉D(fake_image)
real_output
: 기존에 있던 Real Image 를 구분자에 입력시켜 판별된 값, 즉D(real_image)
⏳ fake_output
과 real_output
을 tf.ones_like()
와 tf.zeros_like()
함수를 이용해서 각각 1 또는 0 을 비교해보자.
이 함수들은 특정 벡터와 동일한 크기이면서 값은 1 또는 0 으로 가득 채워진 벡터를 만들고 싶을 때 사용한다.
vector = [
[1, 2, 3],
[4, 5, 6]
]
tf.ones_like(vector)
<tf.Tensor: shape=(2, 3), dtype=int32, numpy= array([[1, 1, 1], [1, 1, 1]], dtype=int32)>
⏳ 입력해 준 vector
와 형태는 같지만, 내용물은 모두 1인 벡터가 만들어졌다.
generator_loss
generator_loss
는 fake_output
이 1에 가까워지기를 바라므로, 다음과 같이 tf.ones_like()
와의
교차 엔트로피 값을 계산하면 된다.
즉, cross_entropy(tf.ones_like(fake_output), fake_output)
값은
fake_output 이 (Real Image 를 의마하는) 1에 가까울수록 작은 값을 가진다.
⤵⤵⤵
def generator_loss(fake_output):
return cross_entropy(tf.ones_like(fake_output), fake_output)
discriminator_loss
discriminator_loss
는 real_output
값은 1에 가까워지기를,
fake_output
값은 0에 가까워지기를 바라므로, 두 가지 loss값을 모두 계산한다.
real_output
은 1로 채워진 벡터와, fake_output
은 0으로 채워진 벡터와 비교하면 된다.
최종 discriminator_loss
값은 이 둘을 더한 값이다.
⤵⤵⤵
def discriminator_loss(real_output, fake_output):
real_loss = cross_entropy(tf.ones_like(real_output), real_output)
fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output)
total_loss = real_loss + fake_loss
return total_loss
discriminator accuracy
판별자가 real output, fake output 을 얼마나 정확히 판별하는지의 accuracy 를 계산하는 것도 GAN의 학습에서 중요하다.
특히 두 accuracy 를 따로 계산해서 비교하는 것이 매우 유용할 것이다.
만약 판별자가 real output 과 fake output 을 accuracy 가 1.0 에 가까울 정도로 정확하게 판별하는 것이 좋은걸까????
그렇지 않다. 이 경우 생성자가 만들어내는 fake output 이 real output 과 차이가 많이 나기 때문에
판별자가 매우 쉽게 판별해 내고 있다는 뜻이다.
그래서, real accuracy 와 fake accuracy 는 초반에는 1.0 에 가깝게 나오다가, 서서히 낮아져 둘 다 0.5에 가까워지는 것이 이상적이다.
fake accuracy 가 1.0 에 더 가깝다면 아직은 생성자가 판별자를 잘 속이지 못하고 있다는 뜻이다.
⤵⤵⤵
def discriminator_accuracy(real_output, fake_output):
real_accuracy = tf.reduce_mean(tf.cast(tf.math.greater_equal(real_output, tf.constant([0.5])), tf.float32))
fake_accuracy = tf.reduce_mean(tf.cast(tf.math.less(fake_output, tf.constant([0.5])), tf.float32))
return real_accuracy, fake_accuracy
⏳ 위 함수 안에 사용된 tensorflow 함수들의 역할을 순차적으로 예를 들어 정리하면 다음과 같다.
ex.) real_output = tf.Tensor([0.2, 0.4, 0.7, 0.9]) 라면,
- tf.math.greater_equal(real_output, tf.constant([0.5]) :
- real_output의 각 원소가 0.5 이상인지 True, False로 판별 - >> tf.Tensor([False, False, True, True])
- tf.cast( (1), tf.float32) :
1
. 의 결과가 True이면 1.0, False이면 0.0으로 변환 - >> tf.Tensor([0.0, 0.0, 1.0, 1.0])
- tf.reduce_mean( 2. ) :
2
. 의 결과를 평균내어 이번 배치의 정확도(accuracy)를 계산 - >> 0.5
최적화 함수(optimizer)
Adam 최적화 기법 을 활용해서 만들어보자.
Adam 함수 또한 tf.keras.optimizers
안에 있다.
중요한 하이퍼 파라미터인 "learning rate"는 0.0001로 설정 하고,
학습 품질을 올려보고 싶다면 여러 가지로 값을 바꾸어 가며 학습을 진행해 보자.
중요한 점 한 가지는 생성자와 구분자는 따로따로 학습을 진행하는 개별 네트워크이기 때문에
optimizer를 따로 만들어주어야 한다는 점이다.
⤵⤵⤵
generator_optimizer = tf.keras.optimizers.Adam(1e-4)
discriminator_optimizer = tf.keras.optimizers.Adam(1e-4)
⏳ 매번 학습이 어떻게 진행되고 있는지 확인하기 위해 생성자가 생성한 샘플을 확인해 보자.
⏳ 샘플은 한번에 16장 생성
생성할 샘플은 같은 노이즈로 생성해야 그에 대한 진전 과정을 확인할 수 있어서, 고정된 seed 노이즈를 만들어야 한다.
100차원의 노이즈를 총 16개, (16, 100)
형상의 벡터를 만들어보자.
⤵⤵⤵
noise_dim = 100
num_examples_to_generate = 16
seed = tf.random.normal([num_examples_to_generate, noise_dim])
seed.shape
TensorShape([16, 100])
5-2. 훈련과정 설계¶
이제 훈련을 위한 코드를 만들어보자.
하나의 미니 배치 당 진행할 train_step
함수를 먼저 만들어야 한다.
Tensorflow 2.0 이후부터는 1.X 의 이전 버전과 다르게, session 을 사용하지 않는다.
대신, 학습시킬 훈련 함수 위에 @tf.function
이라는 데코레이터를 붙여서 사용하면 된다.
데코레이터는 우리가 직접 session 을 열어서 학습했다가, 학습이 완료되면 다시 닫아주는 등의 번거로운 과정을
내부적으로 처리해서 편리하게 학습이 가능하다.
데코레이터 @tf.function
에 대한 명확한 역할은 Tensorflow Tutorial에 정의 되어있다.
tf.function: Compiles a function into a callable TensorFlow graph
(함수를 호출 가능한 TensorFlow 그래프로 컴파일한다.)
아래 두 코드를 보면 위의 정의가 명확해 질 것이다.
⤵⤵⤵
import numpy as np
import tensorflow as tf
def f(x, y):
print(type(x))
print(type(y))
return x ** 2 + y
x = np.array([2, 3])
y = np.array([3, -2])
f(x, y)
<class 'numpy.ndarray'> <class 'numpy.ndarray'>
array([7, 7])
@tf.function # 위와 동일한 함수이지만 #tf.function 데코레이터가 적용
def f(x, y):
print(type(x))
print(type(y))
return x ** 2 + y
x = np.array([2, 3])
y = np.array([3, -2])
f(x, y)
<class 'tensorflow.python.framework.ops.Tensor'> <class 'tensorflow.python.framework.ops.Tensor'>
<tf.Tensor: shape=(2,), dtype=int64, numpy=array([7, 7])>
⏳ Numpy array 입력 x, y 를 동일하게 사용했지만 f(x, y) 의 결과 타입은 다르다.
@tf.function
데코레이터가 사용된 함수에 입력된 입력은
Tensorflow 의 graph 노드가 될 수 있는 타입으로 자동 변환된다.
@tf.function
def train_step(images): #(1) 입력데이터
noise = tf.random.normal([BATCH_SIZE, noise_dim]) #(2) 생성자 입력 노이즈
with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape: #(3) tf.GradientTape() 오픈
generated_images = generator(noise, training=True) #(4) generated_images 생성
#(5) discriminator 판별
real_output = discriminator(images, training=True)
fake_output = discriminator(generated_images, training=True)
#(6) loss 계산
gen_loss = generator_loss(fake_output)
disc_loss = discriminator_loss(real_output, fake_output)
#(7) accuracy 계산
real_accuracy, fake_accuracy = discriminator_accuracy(real_output, fake_output)
#(8) gradient 계산
gradients_of_generator = gen_tape.gradient(gen_loss, generator.trainable_variables)
gradients_of_discriminator = disc_tape.gradient(disc_loss, discriminator.trainable_variables)
#(9) 모델 학습
generator_optimizer.apply_gradients(zip(gradients_of_generator, generator.trainable_variables))
discriminator_optimizer.apply_gradients(zip(gradients_of_discriminator, discriminator.trainable_variables))
return gen_loss, disc_loss, real_accuracy, fake_accuracy #(10) 리턴값
⏳ train_step
함수를 하나하나 뜯어보자.
- 입력 데이터: Real Image 역할을 할 images 한 세트를 입력으로 받음
- 생성자 입력 노이즈: generator가 FAKE IMAGE를 생성하기 위한 noise를 images 한 세트와 같은 크기인 BATCH_SIZE 만큼 생성함
tf.GradientTape()
는 가중치 갱신을 위한 Gradient를 자동 미분으로 계산하기 위해 with 구문 열기
- generated_images 생성: generator가 noise를 입력받은 후 generated_images 생성
- discriminator 판별: discriminator가 Real Image인 images와 Fake Image인 generated_images를 각각 입력받은 후
real_output, fake_output 출력
- loss 계산: fake_output, real_output으로 generator와 discriminator 각각의 loss 계산
- accuracy 계산: fake_output, real_output으로 discriminator의 정확도 계산
- gradient 계산: gen_tape와 disc_tape를 활용해 gradient를 자동으로 계산
- 모델 학습: 계산된 gradient를 optimizer에 입력해 가중치 갱신
- 리턴값: 이번 스텝에 계산된 loss와 accuracy를 리턴
⏳ 한 단계식 학습할 train_step 과 함께 일정 간격으로 학습 현황을 볼 수 있는 함수를 만들어보자.
⏳ 만들어 놓은 고정된 seed 를 이용해서 결과물을 만들어내므로, 고정된 seed 에 대한 결과물이 개선되는 모습이 확인 가능하다.
def generate_and_save_images(model, epoch, it, sample_seeds):
predictions = model(sample_seeds, training=False)
fig = plt.figure(figsize=(4, 4))
for i in range(predictions.shape[0]):
plt.subplot(4, 4, i + 1)
plt.imshow(predictions[i, :, :, 0], cmap='gray')
plt.axis('off')
plt.savefig('/Users/beatelfeed/aiffel/dcgan_newimage/fashion/generated_samples/sample_epoch_{:04d}_iter_{:03d}.png'.format(epoch, it))
plt.show()
⏳ model 이 16개의 seed 가 들어있는 sample_seeds
를 입력받아 만든 prediction
을 matplotlib 으로 시각화.
⏳ subplot 을 이용해서 총 16개의 sample 을 시각화하는 과정으로 4 행 4 열로 나누어 subplot(4, 4, i + 1)
로 시각화.
⏳ plt
에 저장되어 이미지를 plt.savefig
로 파일 저장
⏳ 학습 과정을 체크하기 위해 시각화를 해야되는 중요한 것은 loss 와 accuracy 그래프이다.
⏳ GAN 은 두 모델이 서로 학습 과정에 영향을 주기때문에 지도학습 모델보다 까다롭다.
⏳ train_step()
함수가 리턴하는 gen_loss
, disc_loss
, real_accuracy
, fake_accuracy
4 가지 값을 history 의 dict 구조에 리스트로 저장한 후 매번 epoch 마다 시가화 하는 함수를 만들어보자.
⏳ 생성자의 loss 의 history 는 history['gen_loss']
로 접근 가능한 list로 관리
⤵⤵⤵
from matplotlib.pylab import rcParams
rcParams['figure.figsize'] = 15, 6 # matlab 차트의 기본 크기를 15, 6 으로 지정
def draw_train_history(history, epoch):
# summarize history for loss
plt.subplot(211)
plt.plot(history['gen_loss'])
plt.plot(history['disc_loss'])
plt.title('model loss')
plt.ylabel('loss')
plt.xlabel('batch iters')
plt.legend(['gen_loss', 'disc_loss'], loc='upper left')
# summarize history for accuracy
plt.subplot(212)
plt.plot(history['fake_accuracy'])
plt.plot(history['real_accuracy'])
plt.title('discriminator accuracy')
plt.xlabel('batch iters')
plt.ylabel('accuracy')
plt.legend(['fake_accuracy', 'real_accuracy'], loc='upper left')
# training_history 디렉토리에 epoch 별로 그래프를 이미지 파일로 저장
plt.savefig('/Users/beatelfeed/aiffel/dcgan_newimage/fashion/training_history/train_history_{:04d}.png'.format(epoch))
plt.show()
⏳ 정기적으로 모델을 저장하기 위한 checkpoint 를 만들어주자.
tf.tran.Checkpoint
를 활용하면 매번 모델을 직접 저장하지 않아도, 편하게 버전 관리가 가능하다.
⏳ checkpoint 에는 optimizer 와 생성자, 판별자를 모두 넣어 저장한다.
정확히는 생성자와 판별자가 학습한 모델 가중치를 저장하는 것이다.
⏳ checkpoin 모델을 저장하기 위해 training_checkpoints
디렉토리를 만들자.
⤵⤵⤵
checkpoint_dir = '/Users/beatelfeed/aiffel/dcgan_newimage/fashion/training_checkpoints'
checkpoint_prefix = os.path.join(checkpoint_dir, "ckpt")
checkpoint = tf.train.Checkpoint(generator_optimizer=generator_optimizer,
discriminator_optimizer=discriminator_optimizer,
generator=generator,
discriminator=discriminator)
5-3. 학습 시키기¶
지금까지 위에서 한 단계를 학습하는 train_step
,
샘플 이미지를 생성하고 저장하기 위한 generate_and_save_images()
,
학습과정을 시각화하는 draw_train_history()
,
모델까지 저장하기 위한 checkpoint
를 준비하였다.
🔥 이 모든 것을 한곳에 합쳐주자.⤵⤵⤵
def train(dataset, epochs, save_every):
start = time.time()
history = {'gen_loss':[], 'disc_loss':[], 'real_accuracy':[], 'fake_accuracy':[]}
for epoch in range(epochs):
epoch_start = time.time()
for it, image_batch in enumerate(dataset):
gen_loss, disc_loss, real_accuracy, fake_accuracy = train_step(image_batch)
history['gen_loss'].append(gen_loss)
history['disc_loss'].append(disc_loss)
history['real_accuracy'].append(real_accuracy)
history['fake_accuracy'].append(fake_accuracy)
if it % 50 == 0:
display.clear_output(wait=True)
generate_and_save_images(generator, epoch+1, it+1, seed)
print('Epoch {} | iter {}'.format(epoch+1, it+1))
print('Time for epoch {} : {} sec'.format(epoch+1, int(time.time()-epoch_start)))
if (epoch + 1) % save_every == 0:
checkpoint.save(file_prefix=checkpoint_prefix)
display.clear_output(wait=True)
generate_and_save_images(generator, epochs, it, seed)
print('Time for training : {} sec'.format(int(time.time()-start)))
draw_train_history(history, epoch)
⏳ 모델을 저장하는 간격은 save_every
와 전체 학습 에폭을 결정하는 EPOCHS
파라미터 설정 후 훈련을 해보자.
⏳ 다음과 같이 train()
함수를 실행시켜 모델이 학습하는 결과물을 실시간으로 확인이 가능하다.
save_every = 5
EPOCHS = 50
# 사용가능한 GPU 디바이스 확인
tf.config.list_physical_devices("GPU")
[]
%%time
train(train_dataset, EPOCHS, save_every)
# 진행 과정을 수시로 확인해 보자
Time for training : 10855 sec
CPU times: user 11h 34min 42s, sys: 1h 38min 11s, total: 13h 12min 53s Wall time: 3h 55s
⏳ 학습이 성공적으로 진행되었다
⏳ 만약 Fake Image 에 대한 판별자의 Accuracy(fake_accuracy) 가 계속 1에 가깝게 유지되고 있다면,
생성자가 만든 이미지가 아직 판별자를 성공적으로 속이지 못하고 있다는 뜻이다.
⏳ 더욱 많은 Epoch으로 학습을 시켜서 새로운 디자인의 패션을 만들어보는 것도 가능하다.
🔥 단순 학습 수행 시간만 늘리는 것보다, 모델 구조나 학습 방법을 바꾸는 것도 도움이 될 것이다.
그래프를 유심히 보면서 학습 결과가 더 이상 개선되고 있지 않은지 여부를 확인하는 것도 좋을 것 같다.
학습과정 시각화하기
학습이 끝난 후 생성했던 샘플 이미지들을 합쳐서 GIF 로 만들어보자.
GIF 파일은 import imageio
라이브러리를 활용하면 된다.
imageio.get_writer
를 활용해서 파일을 열고, 거기에 append_data
로 이미지를 하나씩 붙여나가는 방식이다.
anim_file = '/Users/beatelfeed/aiffel/dcgan_newimage/fashion/fashion_mnist_dcgan.gif'
with imageio.get_writer(anim_file, mode='I') as writer:
filenames = glob.glob('/Users/beatelfeed/aiffel/dcgan_newimage/fashion/generated_samples/sample*.png')
filenames = sorted(filenames)
last = -1
for i, filename in enumerate(filenames):
frame = 2*(i**0.5)
if round(frame) > round(last):
last = frame
else:
continue
image = imageio.imread(filename)
writer.append_data(image)
image = imageio.imread(filename)
writer.append_data(image)
!ls -l ~/aiffel/dcgan_newimage/fashion/fashion_mnist_dcgan.gif
-rw-r--r-- 1 beatelfeed staff 967365 2 11 14:51 /Users/beatelfeed/aiffel/dcgan_newimage/fashion/fashion_mnist_dcgan.gif
anim_file = '/Users/beatelfeed/aiffel/dcgan_newimage/fashion/fashion_loss_accuracy.gif'
with imageio.get_writer(anim_file, mode='I') as writer:
filenames = glob.glob('/Users/beatelfeed/aiffel/dcgan_newimage/fashion/training_history/train*.png')
filenames = sorted(filenames)
last = -1
for i, filename in enumerate(filenames):
frame = 2*(i**0.5)
if round(frame) > round(last):
last = frame
else:
continue
image = imageio.imread(filename)
writer.append_data(image)
image = imageio.imread(filename)
writer.append_data(image)
!ls -l ~/aiffel/dcgan_newimage/fashion/fashion_loss_accuracy.gif
-rw-r--r-- 1 beatelfeed staff 637415 2 11 14:56 /Users/beatelfeed/aiffel/dcgan_newimage/fashion/fashion_loss_accuracy.gif
🔥 다음은 CIFAR-10 Datasets 을 활용해여 이미지 생성을 해보자.
쓩~~~
'인공지능' 카테고리의 다른 글
[Part 2]CIFAR-10 을 활용한 이미지 생성기 (0) | 2022.02.16 |
---|---|
[Part 1]CIFAR-10 을 활용한 이미지 생성기 (1) | 2022.02.15 |
[Part 1]인공지능으로 새로운 패션을 만들 수 있다! (0) | 2022.02.11 |
Tensorflow(TF)_V2_API (0) | 2022.01.29 |
감성 분석[Emotional analysis] (0) | 2022.01.28 |