소소하지만 소소하지 않은 개발 공부/머신 러닝 교과서

[머신러닝교과서] GAN 모델 구현하기(2)

still..epochs 2023. 2. 3. 17:45

* 본 포스팅은 머신러닝교과서를 참조하여 작성되었습니다.

* https://github.com/rickiepark/python-machine-learning-book-3rd-edition

 

GitHub - rickiepark/python-machine-learning-book-3rd-edition: <머신 러닝 교과서 3판>의 코드 저장소

<머신 러닝 교과서 3판>의 코드 저장소. Contribute to rickiepark/python-machine-learning-book-3rd-edition development by creating an account on GitHub.

github.com

 

 

GAN 모델 훈련하기

손실 함수를 위해 BinaryCrossentropy 클래스 객체를 만들어 앞서 처리한 배치에 대해 생성자와 판별자의 손실을 계산한다. 이를 위해 각 출력에 대한 정답 레이블이 필요하다. 생성자를 위해서는 1로 채워진 벡터를만든다. 이 벡터의 크기는 생성된 이미지의 로짓 값을 담은 벡터 d_logits_fake와 크기가 같다. 판별자는 두 개의 손실이 필요하다. d_logits_fake를 사용하여 가짜 샘플을 감지하는 손실을 계산하고 d_logts_fake를 기반으로 진짜 샘플을 감지하는 손실을 계산한다.

 

가짜 샘플을 위한 정답 레이블은 0으로 채워진 벡터로 tf.zeors()(또는 tf.zeros_like())함수를 사용하여 만들 수 있다. 비슷하게 진짜 이미지를 위한 정답으로 tf.ones()(또는 tf.ones_like())함수를 사용하여 1로 채워진 벡터를 만들 수 있다.

loss_fn = tf.keras.losses.BinaryCrossentropy(from_logits=True)
## 생성자 손실
g_labels_real = tf.ones_like(d_logits_fake)
g_loss = loss_fn(y_true=g_labels_real, y_pred=d_logits_fake)
print('생성자 손실: {:.4f}'.format(g_loss))

>> 생성자 손실: 0.7019

## 판별자 손실
d_labels_real = tf.ones_like(d_logits_real)
d_labels_fake = tf.zeros_like(d_logits_fake)
d_loss_real = loss_fn(y_true=d_labels_real,
                      y_pred=d_logits_real)
d_loss_fake = loss_fn(y_true=d_labels_fake,
                      y_pred=d_logits_fake)
print('판별자 손실: 진짜 {:.4f} 가짜 {:.4f}'.format(d_loss_real.numpy(), d_loss_fake.numpy()))

>> 판별자 손실: 진짜 1.4667 가짜 0.6871

 

다음 코드에서는 GAN 모델을 만들고 훈련 반복을 위해 for 반복문 안에서 이런 계산을 수행한다.

 

또한, tf.GradientTape() 로 모델의 가중치에 대한 손실의 그레이디언트를 계산하고 두 개의 Adam 옵티마이저를 사용하여 생성자와 판별자의 파라미터를 최적화한다.

# gpu 설정
import tensorflow as tf
print(tf.__version__)

print("GPU 여부:", len(tf.config.list_physical_devices('GPU')) > 0)

if tf.config.list_physical_devices('GPU'):
    device_name = tf.test.gpu_device_name()
else:
    device_name = 'cpu:0'
    
print(device_name)


import time

num_epochs = 100
batch_size = 64
image_size = (28, 28)
z_size = 20
mode_z = 'uniform'
gen_hidden_layers = 1
gen_hidden_size = 100
disc_hidden_layers = 1
disc_hidden_size = 100

tf.random.set_seed(1)
np.random.seed(1)


if mode_z == 'uniform':
    fixed_z = tf.random.uniform(
        shape=(batch_size, z_size),
        minval=-1, maxval=1)
elif mode_z == 'normal':
    fixed_z = tf.random.normal(
        shape=(batch_size, z_size))


def create_samples(g_model, input_z):
    g_output = g_model(input_z, training=False)
    images = tf.reshape(g_output, (batch_size, *image_size))    
    return (images+1)/2.0

## 데이터셋 준비
mnist_trainset = mnist['train']
mnist_trainset = mnist_trainset.map(
    lambda ex: preprocess(ex, mode=mode_z))

mnist_trainset = mnist_trainset.shuffle(10000)
mnist_trainset = mnist_trainset.batch(
    batch_size, drop_remainder=True)

## 모델 준비
with tf.device(device_name):
    gen_model = make_generator_network(
        num_hidden_layers=gen_hidden_layers, 
        num_hidden_units=gen_hidden_size,
        num_output_units=np.prod(image_size))
    gen_model.build(input_shape=(None, z_size))

    disc_model = make_discriminator_network(
        num_hidden_layers=disc_hidden_layers,
        num_hidden_units=disc_hidden_size)
    disc_model.build(input_shape=(None, np.prod(image_size)))

## 손실 함수와 옵티마이저:
loss_fn = tf.keras.losses.BinaryCrossentropy(from_logits=True)
g_optimizer = tf.keras.optimizers.Adam()
d_optimizer = tf.keras.optimizers.Adam()

all_losses = []
all_d_vals = []
epoch_samples = []

start_time = time.time()
for epoch in range(1, num_epochs+1):
    epoch_losses, epoch_d_vals = [], []
    for i,(input_z,input_real) in enumerate(mnist_trainset):
        
        ## 생성자 손실을 계산합니다
        with tf.GradientTape() as g_tape:
            g_output = gen_model(input_z)
            d_logits_fake = disc_model(g_output, training=True)
            labels_real = tf.ones_like(d_logits_fake)
            g_loss = loss_fn(y_true=labels_real, y_pred=d_logits_fake)
            
        # g_loss의 그래디언트를 계산합니다
        g_grads = g_tape.gradient(g_loss, gen_model.trainable_variables)
        
        # 최적화: 그래디언트를 적용합니다
        g_optimizer.apply_gradients(
            grads_and_vars=zip(g_grads, gen_model.trainable_variables))

        ## 판별자 손실을 계산합니다
        with tf.GradientTape() as d_tape:
            d_logits_real = disc_model(input_real, training=True)

            d_labels_real = tf.ones_like(d_logits_real)
            
            d_loss_real = loss_fn(
                y_true=d_labels_real, y_pred=d_logits_real)

            d_logits_fake = disc_model(g_output, training=True)
            d_labels_fake = tf.zeros_like(d_logits_fake)

            d_loss_fake = loss_fn(
                y_true=d_labels_fake, y_pred=d_logits_fake)

            d_loss = d_loss_real + d_loss_fake

        ## d_loss의 그래디언트를 계산합니다
        d_grads = d_tape.gradient(d_loss, disc_model.trainable_variables)
        
        ## 최적화: 그래디언트를 적용합니다
        d_optimizer.apply_gradients(
            grads_and_vars=zip(d_grads, disc_model.trainable_variables))
                           
        epoch_losses.append(
            (g_loss.numpy(), d_loss.numpy(), 
             d_loss_real.numpy(), d_loss_fake.numpy()))
        
        d_probs_real = tf.reduce_mean(tf.sigmoid(d_logits_real))
        d_probs_fake = tf.reduce_mean(tf.sigmoid(d_logits_fake))
        epoch_d_vals.append((d_probs_real.numpy(), d_probs_fake.numpy()))        
    all_losses.append(epoch_losses)
    all_d_vals.append(epoch_d_vals)
    print(
        '에포크 {:03d} | 시간 {:.2f} min | 평균 손실 >>'
        ' 생성자/판별자 {:.4f}/{:.4f} [판별자-진짜: {:.4f} 판별자-가짜: {:.4f}]'
        .format(
            epoch, (time.time() - start_time)/60, 
            *list(np.mean(all_losses[-1], axis=0))))
    epoch_samples.append(
        create_samples(gen_model, fixed_z).numpy())

 

모델 훈련이 긑난 후 판별자와 생성자 손실을 그래프로 출력하여 두 신경망의 훈련 과정을 분석하고 수렴하는지 평가 해 보는 것이 좋다.

 

또한, 반복마다 판별자에서 계산한 진짜 샘플과 가짜 샘플의 평균 확률을 출력하는 것이 도움이 된다. 이 확률이 0.5 근처라면 판별자가 진짜와 가짜 이미지를 잘 구분할 수 없다는 것을 의미한다.

 

import itertools
fig = plt.figure(figsize=(16, 6))
## 손실 그래프
ax = fig.add_subplot(1, 2, 1)
g_losses = [item[0] for item in itertools.chain(*all_losses)]
d_losses = [item[1]/2.0 for item in itertools.chain(*all_losses)]
plt.plot(g_losses, label='Generator loss', alpha=0.95)
plt.plot(d_losses, label='Discriminator loss', alpha=0.95)
plt.legend(fontsize=20)
ax.set_xlabel('Iteration', size=15)
ax.set_ylabel('Loss', size=15)

epochs = np.arange(1, 101)
epoch2iter = lambda e: e*len(all_losses[-1])
epoch_ticks = [1, 20, 40, 60, 80, 100]
newpos= [epoch2iter(e) for e in epoch_ticks]
ax2 = ax.twiny()
ax2.set_xticks(newpos)
ax2.set_xticklabels(epoch_ticks)
ax2.xaxis.set_ticks_position('bottom')
ax2.xaxis.set_label_position('bottom')
ax2.spines['bottom'].set_position(('outward', 60))
ax2.set_xlabel('Epoch', size=15)
ax2.set_xlim(ax.get_xlim())
ax.tick_params(axis='both', which='major', labelsize=15)
ax2.tick_params(axis='both', which='major', labelsize=15)

## 판별자의 출력
ax = fig.add_subplot(1, 2, 2)
d_vals_real = [item[0] for item in itertools.chain(*all_d_vals)]
d_vals_fake = [item[1] for item in itertools.chain(*all_d_vals)]
plt.plot(d_vals_real, alpha=0.75, label=r'Real: $D(\mathbf{x})$')
plt.plot(d_vals_fake, alpha=0.75, label=r'Fake: $D(G(\mathbf{z}))$')
plt.legend(fontsize=20)
ax.set_xlabel('Iteration', size=15)
ax.set_ylabel('Discriminator output', size=15)
ax2 = ax.twiny()
ax2.set_xticks(newpos)
ax2.set_xticklabels(epoch_ticks)
ax2.xaxis.set_ticks_position('bottom')
ax2.xaxis.set_label_position('bottom')
ax2.spines['bottom'].set_position(('outward', 60))
ax2.set_xlabel('Epoch', size=15)
ax2.set_xlim(ax.get_xlim())
ax.tick_params(axis='both', which='major', labelsize=15)
ax2.tick_params(axis='both', which='major', labelsize=15)
plt.show()

GAN 훈련 결과

판별자 출력을 보면 알 수 있듯이 훈련 초기에는 판별자가 진짜와 가짜 샘플을 매우 정확하게 구별하는 법을 빠르게 배운다. 즉, 가짜 샘플의 확률이 0에 가깝다. 가짜 샘플이 전혀 진짜 샘플과 닮지 않았기 때문이다. 따라서 진짜와 가짜 샘플을 구별하는 일은 비교적 쉽다. 훈련이 진행되면서 생성자가 더 진짜 같은 이미지를 합성하게 되면 진짜와 가짜 샘플에 대한 확률이 모두 0.5에 가까워진다.

 

훈련하는 동안 생성자의 출력, 즉 합성된 이미지가 어떻게 변하는지 확인해 볼 수 있다. 에포크가 끝날 때마다 create_samples() 를 호출해서 몇 개의 샘플을 생성하고 파이썬 리스트에 저장했다. 다음 코드에서 적절한 에포크 선택을 위해 생성자가 만든 이미지를 그려보자.

selected_epochs = [1, 2, 4, 10, 50, 100]
fig = plt.figure(figsize=(10, 14))
for i,e in enumerate(selected_epochs):
    for j in range(5):
        ax = fig.add_subplot(6, 5, i*5+j+1)
        ax.set_xticks([])
        ax.set_yticks([])
        if j == 0:
            ax.text(
                -0.06, 0.5, 'Epoch {}'.format(e),
                rotation=90, size=18, color='red',
                horizontalalignment='right',
                verticalalignment='center', 
                transform=ax.transAxes)
        
        image = epoch_samples[e-1][j]
        ax.imshow(image, cmap='gray_r')

plt.show()

 

위 그림에서 볼 수 있듯이 생성자 신경망은 훈련이 진행될수록 더 실제 같은 이미지를 만든다. 

 

이번에는 생성자와 판별자에 완전 연결 은닉층 하나만 가진 매우 단순한 GAN 모델을 만들었다. 이 GAN 모델을 MNIST 데이터셋에서 훈련한 후 아주 만족스럽지는 않지만 가능성이 있어 보이는 새로운 손글씨 숫자를 만들었다. 15장에서 배웠듯 합성곱 층을 사용한 신경망은 이미지 분류에서 완전 연결 층보다 몇 가지 장점이 있다. 비슷한 이유로 이미지 데이터를 다루는 GAN 모델에 합성곱 층을 추가하면 더 나은 결과를 얻을 수 있다.