GAN 기초 : 적대적 훈련
생성자와 판별자는 신경망과 같은 미분가능한 함수로 표현된다. 이 신경망은 각자 자신만의 비용 함수를 가진다. 판별자의 손실을 사용하여 역잔파학습으로 두 네트워크를 훈련하는데 판별자는 진짜와 가짜 샘플에 대한 손실을 최소화하려 노력하고 생성자는 자신이 생성한 가짜 샘플에 대한 판별자의 손실이 최대화가 되도록 노력한다.
생성자가 흉내 내려는 샘플의 종류는 훈련 데이터셋으로 결정되고 결국 생성자의 목적은 훈련 데이터셋의 데이터 분포를 흉내내는 샘플을 생성하는 것이다.
비용 함수
전통적인 신경망의 비용함수와의 차이점은 기존의 신경망은 한 개의 훈련 파라미터에 의해 결정되었다면 GAN은 생성자와 판별자 파라미터에 의해 결정된다.
파라미터가 2개라는 것은 단순히 이 비용함수를 결정짓는 요소가 한 개 늘어난 것 뿐만 아니라 이 두 개의 파라미터들이 서로 상호작용하는 경우도 있기에 더 복잡한 구조를 만들게 된다.
훈련 과정
전통적인 신경망의 훈련은 최적화 문제이다. 파라미터 공간에서 비용이 최소화 되는 점으로 이동하여 해당 파라미터를 찾아 최적화한다. 이때 비용함수는 전역 최소점 또는 지역 최소점에 도달하게 된다.
GAN은 생성자와 판별자는 자신의 파라미터만 튜닝하고 상대방의 파라미터는 튜닝할 수 없다. 이 특성으로 두 네트워크는 내시 균형에 도달하게 된다. 내시 균형이란 둘 중 어느 하나도 더 좋은 상황으로 변환할 수 없는 상태를 말한다.
오차 행렬
판별자의 분류는 오차 행렬로 표현 할 수 있다.
- 진짜 양성 : 진짜로 분류한 진짜 샘플. = 1
- 거짓 음성 : 가짜로 분류한 진짜 샘플. = 0
- 진짜 음성 : 가짜로 분류한 가짜 샘플. = 0
- 거짓 양성 : 진짜로 분류한 가짜 샘플. = 1
GAN 훈련 알고리즘
1단계 : 판별자 훈련
랜덤한 진짜 샘플의 미니배치 x를 받는다.
랜덤한 잡음 벡터 z의 미니백치를 받고 가짜 샘플의 미니배치를 생성한다. G(z) = x*
D(x)와 D(x*)에 대한 분류 손실을 계산하고 전체 오차를 역전파하여 분류 손실을 최소화되도록 세타(D)를 업데이트 한다.
2단계 : 생성자 훈련
랜덤한 잡음 벡터 z의 미니배치를 받고 가짜 샘플의 미니배치를 생성한다. G(z) = x*
GAN 훈련하기
MNIST 데이터로 진짜 손글씨 숫자처럼 보이는 이미지를 생성하는 GAN을 만들어본다.
먼저 필요한 모듈을 가져온다.
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
from tensorflow.keras.datasets import mnist
from tensorflow.keras.layers import Dense, Flatten, Reshape
from tensorflow.keras.layers import LeakyReLU
from tensorflow.keras.models import Sequential
from tensorflow.keras.optimizers import Adam
모델의 입력과 데이터셋의 차원을 지정한다. MNIST 데이터셋은 하나의 채널을 가진 28x28 픽셀의 이미지이다. 변수 z_dim은 잡음 벡터 z의 크기를 결정한다.
img_rows = 28
img_cols = 28
channels = 1
img_shape = (img_rows, img_cols, channels)
z_dim = 100
그 다음에 생성자와 판별자 네트워크를 구현한다.
먼저 생성자를 구현한다. 간단하게 만들기 위해 생성자는 은닉층을 한 개 가지고 z 벡터를 입력받아 28 x 28 x 1 크기의 이미지를 생성한다. 은닉층은 LeakyReLU 활성화 함수를 사용한다.
일반적인 ReLU 함수는 음수 영역에서 기울기가 0이지만 LeakyReLU 활성화 함수는 음수 영역에서 아주 작은 가중치를 가질 수 있게 하여 훈련 결과를 향상시키는 경향이 있다.
출력층에는 tanh 활성화 함수를 사용하여 출력 값을 [-1,1]의 범위로 조정한다. tanh는 시그모이드보다 기울기 소실문제가 적어 더 정확한 이미지를 만들기에 시그모이드 함수 대신 tanh 활성화 함수를 사용한다.
# 생성자 구현
def build_generator(img_shape, z_dim):
model = Sequential()
model.add(Dense(128, input_dim=z_dim)) # 완전연결층
model.add(LeakyReLU(alpha=0.01)) # LeakyReLU 활성화 함수
model.add(Dense(28 * 28 * 1, activation='tanh'))# tanh 활성화 함수를 사용한 출력층
model.add(Reshape(img_shape)) # 생성자 출력을 이미지 차원으로 변경
return model
다음으로 판별자를 구현한다. 판별자는 28 x 28 x 1 크기의 이미지를 입력 받아 가짜와 비교해 얼마나 진짜인지를 나타내는 확률을 출력하게 한다. 판별자는 2 개의 층으로 구성된 네트워크로 은닉층은 LeakyReLU 활성화 함수와 128개의 은닉 유닛을 가진다. 간단하게 만들기 위해 생성자와 비슷하게 만들었지만 실제로 대부분의 GAN 구현에서 생성자와 판별자 네트워크 구조는 크기와 복잡도가 매우 다르다.
생성자와 달리 판별자의 출력층에는 시그모이드 활성화 함수를 적용했다. 시그모이드 활성화 함수의 출력 값은 [0,1]이므로 판별자가 어떤 이미지를 진짜인지 가짜인지 판단했을 때 그 확률 값을 바로 얻을 수 있게 된다.
# 판별자 구현
def build_discriminator(img_shape):
model = Sequential()
model.add(Flatten(input_shape=img_shape)) # 입력 이미지를 일렬로 펼치기
model.add(Dense(128)) # 완전연결층
model.add(LeakyReLU(alpha=0.01)) # LeakyReLU 활성화 함수
model.add(Dense(1, activation='sigmoid')) # 시그모이드 활성화 함수를 사용한 출력층
return model
이제 구현한 생성자와 판별자를 모델로 만들고 컴파일 한다. 생성자를 훈련하기 위해 연결된 모델에서는 판별자의 파라미터를 동결하기 위해 discriminator.trainable을 False로 지정한다. 판별자를 훈련하지 않도록 설정한 이 연결된 모델은 생성자만 훈련하기 위해 사용된다. 판별자는 독립적으로 컴파일된 모델로 훈련한다.
이진 교차 엔트로피를 훈련하는 동안 최소화할 손실함수로 사용한다. 이진 교차 엔트로피는 두 개의 클래스만 있는 예측에서 계산된 확률과 진짜 확률 사이의 차이를 측정한다. 손실이 클수록 예측이 정답 레이블과 차이가 크다.
각 네트워크를 최적화하기 위해 Adam 최적화 알고리즘을 사용한다. 손실 함수의 손실 값을 어떻게 줄일 것인가를 결정하는 것을 옵티마이저라고 하는데 이번 실습에서는 Adam을 사용한다.
# 모델 생성
def build_gan(generator, discriminator):
model = Sequential()
model.add(generator) # 생성자 + 판별자 모델 연결
model.add(discriminator)
return model
discriminator = build_discriminator(img_shape) # 판별자 모델 생성하고 컴파일 하기
discriminator.compile(loss='binary_crossentropy', optimizer=Adam(), metrics=['accuracy'])
generator = build_generator(img_shape, z_dim) # 생성자 모델 만들기
discriminator.trainable = False # 생성자 훈련 시 판별자 파라미터 동결하기
gan = build_gan(generator, discriminator) # 생성자를 훈련하기 위해 동결한 판별자로 GAN 모델 만들고 컴파일하기
gan.compile(loss='binary_crossentropy', optimizer=Adam())
랜덤한 MNIST 이미지의 미니배치를 진짜 샘플로 받고 랜덤한 잡음 벡터 z로부터 가짜 이미지의 미니배치를 생성한다. 이를 사용해 생성자의 파라미터를 고정한 채로 판별자 네트워크를 훈련한다. 그 다음 가짜 이미지의 미니배치를 생성하고 이를 사용해 판별자의 파라미터를 고정한 채로 생성자를 훈련한다. 이 과정을 계속 반복한다.
# GAN 훈련 반복하기
losses = []
accuracies = []
iteration_checkpoints = []
def train(iterations, batch_size, sample_interval):
(X_train,_), (_,_) = mnist.load_data() # MNIST 데이터셋 로드
X_train = X_train / 127.5 - 1.0 # 픽셀 값을 [0,255]에서 [-1,1]로 조정
X_train = np.expand_dims(X_train, axis=3)
real = np.ones((batch_size, 1)) # 진짜 이미지 레이블 : 모두 1
fake = np.zeros((batch_size, 1)) # 가짜 이미지 레이블 : 모두 0
for iteration in range(iterations):
idx = np.random.randint(0, X_train.shape[0], batch_size) # 진짜 이미지에서 랜덤 배치 가져오기
imgs = X_train[idx]
z = np.random.normal(0, 1, (batch_size,100)) # 가짜 이미지에서 랜덤 배치 가져오기
gen_imgs = generator.predict(z)
d_loss_real = discriminator.train_on_batch(imgs, real) # 판별자 훈련
d_loss_fake = discriminator.train_on_batch(gen_imgs, fake)
d_loss, accuracy = 0.5 * np.add(d_loss_real, d_loss_fake)
z = np.random.normal(0, 1, (batch_size, 100)) # 가짜 이미지 배치 생성
gen_imgs = generator.predict(z)
g_loss = gan.train_on_batch(z, real) # 생성자 훈련
if (iteration+1) % sample_interval == 0:
losses.append((d_loss, g_loss)) # 훈련이 끝난 후 그래프로 나타내기 위해 손실값을 저장
accuracies.append(100.0*accuracy)
iteration_checkpoints.append(iteration+1)
print("%d [D loss: %f, acc.: %.2f%%] [G loss: %f]" % (iteration+1, d_loss, 100.0*accuracy, g_loss)) # 훈련 과정 출력
sample_images(generator) # 생성된 이미지 샘플 출력
생성자 훈련 코드에서 sample_images() 함수를 호출한다. 이 함수는 반복하여 생성자가 합성한 4 x 4 이미지 그리드를 출력한다.
# 생성한 이미지 출력
def sample_images(generator, image_grid_rows=4, image_grid_columns=4):
z = np.random.normal(0, 1, (image_grid_rows * image_grid_columns, z_dim))
gen_imgs = generator.predict(z)
gen_imgs = 0.5 * gen_imgs + 0.5
fig, axs = plt.subplots(image_grid_rows,
image_grid_columns,
figsize=(4,4),
sharey=True,
sharex=True)
cnt = 0
for i in range(image_grid_rows):
for j in range(image_grid_columns):
axs[i,j].imshow(gen_imgs[cnt,:,:,0], cmap='gray')
axs[i,j].axis('off')
cnt+=1
이제 훈련 하이퍼파라미터인 반복 횟수, 배치 크기를 설정하고 모델을 훈련한다. 하이퍼파라미터들은 직접 해보면서 훈련과정이 좋아지거나 나빠지는 것을 시행착오를 통해 결정해야 한다.
하지만 이 숫자들에 대한 실용적인 제약 조건이 있다. 미니 배치는 프로세스 메모리에 들어갈 수 있도록 충분히 작아야 한다. 일반적으로 미니배치의 크기를 2의 배수로 사용한다. 반복 횟수도 제약이 있다. 반복을 많이 할수록 훈련 과정이 길다.
적절한 반복 횟수를 결정하려면 훈련 손실을 모니터링하고 손실이 평탄해지는 부근에서 반복 횟수를 정한다. 이 지점에서는 훈련을 계속하더라도 크게 향상되지 않는다.
GAN은 생성 모델이기 때문에 지도 학습 알고리즘만큼 과대적합을 중요하게 여긴다.
# 모델 실행하기
iterations = 20000
batch_size = 128
sample_interval = 1000
train(iterations, batch_size, sample_interval)
실행 결과
처음 생성자가 생성한 데이터들은 숫자라고는 인식할 수 없을 정도로 잡음 정도만 생성한다. 하지만 훈련이 반복되면서 훈련 데이터의 특징을 점점 더 잘 표현하게 된다. 마지막 이미지에서는 완전히 인식할 정도는 아니지만 그래도 어떤 숫자인지 구별할 수 있을 정도의 숫자를 생성해낸다.
'Book > GAN In Action' 카테고리의 다른 글
| 6장. ProGAN (0) | 2024.08.07 |
|---|---|
| 5장. GAN 평가의 어려움 (0) | 2024.08.06 |
| 4장. DCGAN (0) | 2024.08.01 |
| 2장. 오토인코더와 생성 학습 (0) | 2024.07.29 |
| 1장. GAN과 생성 모델링 (1) | 2024.07.24 |