Go together

If you want to go fast, go alone. If you want to go far, go together.

핸즈온 머신러닝 2판

17장 오토인코더와 GAN을 사용한 표현 학습과 생성적 학습

NowChan 2022. 2. 3. 09:58

autoencoder는 레이블되지 않은 훈련 데이터에서 잠재 표현(latent representation)을 학습할 수 있습니다. 일반적으로 잠재 표현은 입력보다 훨씬 낮은 차원을 가지므로 차원 축소, 특히 시각화에 유용하게 사용됩니다.

 

오토인코더는 DNN의 비지도 사전훈련에 사용될 수 있고, 일부 오토인코더는 훈련 데이터와 비슷한 새로운 데이터를 생성할 수 있습니다. 이를 생성 모델(generative model)이라고 합니다. 생성적 적대 신경망(generative adversarial networks, GAN)은 정말 리얼한 사람 얼굴을 생성합니다. (https://thispersondoesnotexist.com/) 침실을 생성한 이미지를 볼 수도 있습니다. (https://thisrentaldoesnotexist.com/)

 

제 얼굴 아닙니다..

 

GAN은 이미지 해상도 높이기(=초해상도), 이미지 컬러로 바꾸기https://github.com/jantic/DeOldify), 이미지 편집(원하지 않는 배경 바꾸기), 간단한 스케치를 실제 같은 이미지로 바꾸기, 동영상에서 다음 프레임 예측하기, 데이터 증식(argumentation), 텍스트, 오디오, 시계열 같은 여러 다른 종류의 데이터 생성, 다른 모델의 취약점을 식별하고 개선하는 등에 널리 쓰입니다.

 

https://github.com/jantic/DeOldify

 

오토인코더와 GAN은 모두 비지도 학습이며, 둘 다 밀집 표현을 학습하고 생성 모델로 사용할 수 있습니다. 하지만, 작동 방식은 크게 다릅니다.

 

오토인코더는 입력을 출력으로 복사하는 방법을 배우는데, 잠재 표현의 크기를 제한하거나(은닉층의 뉴런 수를 입력층보다 적게함), 입력에 잡음을 추가하는 등 네트워크에 제약을 가해 복사를 어렵게 만듭니다. 이 과정에서 오토인코더는 데이터를 효율적으로 표현하는 방법을 배웁니다.

 

GAN은 신경망 2개로 구성됩니다. 생성자(generator)는 훈련 데이터와 비슷하게 보이는 데이터를 생성합니다. 판별자(discriminator)는 가짜 데이터와 진짜 데이터를 구별합니다. 이 구조는 신경망이 훈련하는 동안 생성자와 판별자가 서로 경쟁한다는 점에서 매우 흥미롭습니다. 얀 르쿤은 적대적 훈련(adversarial training)이 최근 10년 동안 머신러닝 분야에서 가장 흥미로운 아이디어라고도 말했습니다. GAN은 훈련이 매우 어렵습니다.

 

 

효율적인 데이터 표현

 

① 40, 27, 25, 36, 81

② 50, 48, 46, 44, 42, 38, 36, 34, 32, 30

 

①, ② 중 어떤 것이 외우기 쉬울까요? ①이 짧아서 외우기 쉬워보이지만, ②에는 짝수로 줄어드는 패턴이 존재합니다. 보통 긴 시퀀스는 기억하기 어렵기 때문에 패턴을 찾아 기억합니다. 오토인코더가 훈련하는 동안 우리는 제약을 가해서 데이터에 있는 패턴을 찾아 활용할 수 있습니다.

 

기억, 지각, 패턴 매칭 사이의 관계를 연구한 William Chase는 많은 연구를 했습니다.(https://homl.info/111) 이들은 숙련된 체스 플레이어가 체스판을 5초만 보고도 전체 말의 위치를 외울 수 있다는 걸 알아냈습니다. 물론, 실제 게임처럼 현실적인 위치에 있을 경우에 한합니다. 체스 전문가라도 보통 사람보다 뛰어난 기억력을 갖고 있지는 않습니다. 즉, 패턴을 찾으면 효율적으로 정보를 저장할 수 있다는 뜻입니다.

 

 

오토인코더는 두 부분으로 구성됩니다. 입력을 내부 표현으로 바꾸는 인코더(encoder, recognition network), 내부 표현을 출력으로 바꾸는 디코더(decoder, generative network)입니다.

 

위 그림에서는 출력층 뉴런 수가 입력 개수와 같은 MLP(다층 퍼셉트론)과 구조가 동일합니다. 입력을 받아 재구성하기 때문에 출력을 종종 재구성(reconstruction)이라고 부르기도 합니다. 비용 함수는 reconstruction이 입력과 다를 때 벌점을 부과하는 재구성 손실(reconstruction loss)을 포함합니다.

 

내부 표현은 입력보다 저차원으로 만드는 오토인코더를 undercomplete(과소완전)이라고 부릅니다. 이는 입력 데이터에서 가장 중요한 특성을 학습하고 중요하지 않은 것은 버리도록 합니다. 반대로, 입력보다 고차원으로 만드는 오토인코더를 overcomplete라고 부릅니다.

 

 

과소완전 선형 오토인코더로 PCA 수행하기

 

오토인코더가 선형 활성화 함수만 사용하고, 비용 함수가 MSE면 이는 결국 주성분 분석(PCA)을 수행하는 것으로 볼 수 있습니다. 다음 코드는 3D 데이터셋에 PCA를 적용해 2D에 투영하는 간단한 선형 오토 인코더를 만듭니다.

 

np.random.seed(4)

def generate_3d_data(m, w1=0.1, w2=0.3, noise=0.1):
  angles = np.random.rand(m)*3*np.pi/2 - 0.5
  data = np.empty((m, 3))
  data[:, 0] = np.cos(angles) + np.sin(angles)/2 + noise * np.random.randn(m)/2
  data[:, 1] = np.sin(angles) + 0.7 + noise * np.random.randn(m)/2
  data[:, 2] = data[:, 0]*w1 + data[:, 1]*w2 + noise*np.random.randn(m)
  return data

X_train = generate_3d_data(60)
X_train = X_train - X_train.mean(axis=0, keepdims=0)  
# X_train.shape = (60, 3), X_train.mean.shape = (3,) broadcast
encoder = keras.models.Sequential([keras.layers.Dense(2, input_shape=[3])])
decoder = keras.models.Sequential([keras.layers.Dense(3, input_shape=[2])])
autoencoder = keras.models.Sequential([encoder, decoder])

autoencoder.compile(loss='mse', optimizer=keras.optimizers.SGD(lr=0.1))

 

케라스 모델은 다른 모델의 층으로 사용할 수 있다는 것을 기억하세요! 단순히 PCA를 수행하기 위해 활성화 함수를 사용하지 않아서 모든 뉴런이 선형(하나의 뉴런처럼 동작)입니다. 비용 함수는 MSE입니다.

 

import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.scatter(X_train[:, 0], X_train[:, 1], X_train[:, 2])
plt.show()

원본 데이터

 

PCA 처럼 오토인코더는 데이터에 있는 분산이 가장 많이 보존되도록 데이터를 투영할 최상의 2D 평면을 찾습니다.

 

history = autoencoder.fit(X_train, X_train, epochs=20)
codings = encoder.predict(X_train)

fig = plt.figure(figsize=(4, 3))
plt.plot(codings[:, 0], codings[:, 1], 'b.')
plt.xlabel("$z_1$", fontsize=18)
plt.ylabel("$z_2$", fontsize=18, rotation=0)
plt.grid(True)
plt.show()

인코더의 출력, 오토인코더 아님에 주의

 

 

적층 오토인코더

 

다른 신경망처럼 오토인코더도 은닉층을 여러 개 가질 수 있습니다. 이를 적층 오토인코더(stacked autoencoder) 또는 심층 오토인코더(deep autoencoder)라고 합니다. 

 

 

층을 더 추가할 수록 오토인코더가 더 복잡한 코딩(latent representation, 잠재 표현)을 학습할 수 있지만, 너무 많이 추가하면 각 입력 데이터를 임의의 한 숫자로 매핑해버릴 수도 있습니다. 이는 단순히 입력 데이터를 다른 숫자로 매핑한 것에 불과해서 유용한 데이터 표현을 학습하지 못할 것입니다. 다른 샘플에 일반화도 잘 되지 않을 것입니다.

 

적층 오토인코더는 위 그림처럼 전형적으로 가운데 은닉층(코딩 층, 빨간색)을 기준으로 대칭입니다. 예를 들어, 3장에 MNIST를 위한 오토인코더라면 위 그림처럼 입력층(784 units) → 은닉층(100 units) → 은닉층(30 units) → 은닉층(100 units) → 출력층(784 units)을 가집니다.

 

 

케라스를 사용하여 적층 오토인코더 구현하기

 

적층 인코더는 심층 MLP와 매우 비슷하게 구현할 수 있습니다. 다음 코드는 패션 MNIST 데이터셋에서 SELU 활성화 함수를 사용해 적층 오토인코더를 만듭니다.

 

(X_train_full, y_train_full), (X_test, y_test) = keras.datasets.fashion_mnist.load_data()
X_train_full = X_train_full.astype(np.float32)/255
X_test = X_test.astype(np.float32)/255
X_train, X_valid = X_train_full[:-5000], X_train_full[-5000:]
y_train, y_valid = y_train_full[:-5000], y_train_full[-5000:]
stacked_encoder = keras.models.Sequential([
      keras.layers.Flatten(input_shape=[28, 28]),
      keras.layers.Dense(100, activation='selu'),
      keras.layers.Dense(30, activation='selu'),
])
stacked_decoder = keras.models.Sequential([
      keras.layers.Dense(100, activation='selu', input_shape=[30]),
      keras.layers.Dense(28*28, activation='sigmoid'),
      keras.layers.Reshape([28, 28])
])
stacked_ae = keras.models.Sequential([stacked_encoder, stacked_decoder])
stacked_ae.compile(loss='binary_crossentropy',
                   optimizer=keras.optimizers.SGD(lr=1.5))
history = stacked_ae.fit(X_train, X_train, epochs=10,
                         validation_data=(X_valid, X_valid))

 

인코더에서 두 Dense 층은 모두 SELU 활성화 함수를 사용합니다. 르쿤 초기화를 사용할 수 있지만 네트워크가 아주 깊지 않아서 큰 차이가 없습니다.

 

적층 오토인코더를 컴파일할 때, MSE 대신 이진 크로스 엔트로피 손실을 사용합니다. reconstruction 작업을 다중 이진 분류 문제로 다루는 것입니다. 즉, 각 픽셀의 강도는 픽셀이 검정색일 확률을 나타냅니다. 회귀로 정의하지 않고 다중 이진 분류 문제로 정의하면 모델이 더 빠르게 수렴하는 경향이 있습니다. 정확도를 출력하고 싶을 수 있는데, 타깃과 예측을 0 또는 1 로 반올림한 후 정확도를 계산하는 사용자 정의 지표를 만들어 이 문제를 쉽게 해결할 수 있습니다. 훈련 시에는 X_train을 입력과 타깃으로 사용해 모델을 훈련합니다.

 

 

재구성 시각화

 

입력과 출력을 비교해 적절히 훈련되었는지 확인해보겠습니다.

def plot_image(image):
  plt.imshow(image, cmap='binary')
  plt.axis('off')

def show_reconstructions(model, n_images=5):
  reconstructions = model.predict(X_valid[:n_images])
  fig = plt.figure(figsize=(n_images * 1.5, 3))
  for image_index in range(n_images):
    plt.subplot(2, n_images, 1+image_index)   # plt.subplot(n_rows, n_cols, index)
    plot_image(X_valid[image_index])
    plt.subplot(2, n_images, 1 + n_images + image_index)
    plot_image(reconstructions[image_index])

show_reconstructions(stacked_ae)

 

재구성된 이미지를 식별할 수는 있지만 정보를 조금 많이 잃었습니다. 모델의 epoch를 늘리거나 인코더, 디코더 혹은 코딩 층의 크기를 늘려야할지도 모릅니다. 하지만, 이 또한 너무 크게하면 유익한 패턴을 학습하지 못합니다.

 

 

패션 MNIST 데이터셋 시각화

 

적층 오토인코더는 다른 축소 알고리즘만큼 좋은 시각화를 할 수 없지만, 샘플과 특성이 많은 대용량 데이터셋을 다룰 수 있습니다. 따라서 오토인코더를 사용해 차원을 적절히 축소한 후 다른 차원 축소 알고리즘을 사용해 시각화하는 것도 전략입니다.

 

먼저 적층 오토인코더의 인코더 모델로 차원을 30까지 줄인 후 t-SNE 알고리즘으로 시각화를 위해 2차원까지 줄이겠습니다.

from sklearn.manifold import TSNE

X_valid_compressed = stacked_encoder.predict(X_valid)
tsne = TSNE()
X_valid_2D = tsne.fit_transform(X_valid_compressed)
# X_valid_2D.shape = (5000, 2)
plt.scatter(X_valid_2D[:, 0], X_valid_2D[:, 1], c=y_valid, s=10, cmap='tab10')

 

t-SNE 알고리즘이 식별한 클러스터를 실제 클래스와 비교해 색깔로 나타냈습니다. 우측은 교재에 각 클래스의 그림을 함께 나타낸 것입니다. 어느 정도 잘 매칭되는 것 같습니다. 이렇게 오토인코더를 차원축소에 사용할 수 있습니다.

 

 

적층 오토인코더를 사용한 비지도 사전훈련

 

분류 문제를 푼다고 할 때 레이블된 데이터는 구하기 어렵고, 레이블이 없는 데이터는 간단한 스크립트로 인터넷에서 수 백만개의 이미지를 다운로드 받을 수 있습니다.

 

비슷한 문제를 학습한 신경망이 있다면 하위층을 재사용할 수도 있습니다. 하지만, 레이블되지 않은 대량의 데이터셋이 있다면 먼저 전체 데이터셋을 사용해 적층 오토인코도를 훈련합니다. 그 다음에 오토 인코더의 하위층을 재사용할 수 있습니다. 위 그림을 보면 어떻게 재사용하는지 알 수 있습니다.

 

가령, 분류기를 훈련할 때 훈련 데이터가 많지 않으면 사전훈련된 층을 동결하는 것이 좋습니다.(적어도 가장 하위층 하나) 구현에 특별한 것은 없습니다. 모든 훈련 데이터를 사용해 오토인코더를 훈련하고, 그 다음 인코더 층을 재사용하여 새로운 신경망을 만들면 됩니다.

 

적층 오토인코더를 훈련하기 위한 몇 가지 기술을 살펴봅니다.

 

 

가중치 묶기

 

 

위 그림처럼 오토인코더가 완벽하게 대칭일 때는 디코더의 가중치와 인코더의 가중치를 묶는 것(같은 가중치를 쓰는 것)이 일반적입니다. 파라미터를 절반으로 줄여줘서 훈련 속도와 과대적합 위험을 줄여줍니다.

 

오토인코더가 $N$개의 층을 갖고, $W_L$을 $L$번째 층의 가중치라고 했을 때 디코더 층의 가중치는 $W_{N-L+1}=W_L^T$로 정의할 수 있습니다. 가령, $W_1$은 첫 번째 은닉층의 가중치, $W_{N/2}$는 코딩층입니다. 뉴런 수가 코딩 층을 기준으로 대칭이므로 가중치는 전치해야 행렬곱을 할 수 있습니다.

 

케라스의 사용자 정의층을 만들어 층 간에 가중치를 묶어봅니다.

class DenseTranspose(keras.layers.Layer):
  def __init__(self, dense, activation=None, **kwargs):
    self.dense = dense
    self.activation = keras.activations.get(activation)
    super().__init__(**kwargs)
  def build(self, batch_input_shape):
    self.biases = self.add_weight(name='bias', initializer='zeros',
                                  shape=[self.dense.input_shape[-1]]) #뉴런마다 1개 편향
    super().build(batch_input_shape)
  def call(self, inputs):
    z = tf.matmul(inputs, self.dense.weights[0], transpose_b=True)
    return self.activation(z + self.biases)

 

편향 벡터는 dense로 입력받은 층과는 독자적으로 만들어 사용합니다.

 

Tip

더보기

self.dense.weights[0]을 전치한 후 matmul하는 것보다 tf.matmul에 transpose_b=True로 지정하는 것이 matmul() 연산에서 동적으로 전치를 수행해서 훨씬 효율적입니다.

 

dense_1 = keras.layers.Dense(100, activation='selu')
dense_2 = keras.layers.Dense(30, activation='selu')

tied_encoder = keras.models.Sequential([
          keras.layers.Flatten(input_shape=[28, 28]),
          dense_1,
          dense_2
])

tied_decoder = keras.models.Sequential([
          DenseTranspose(dense_2, activation='selu'),
          DenseTranspose(dense_1, activation='sigmoid'),
          keras.layers.Reshape([28, 28])
])

tied_ae = keras.models.Sequential([tied_encoder, tied_decoder])
tied_ae.compile(loss='binary_crossentropy',
                optimizer=keras.optimizers.SGD(lr=1.5))
history = tied_ae.fit(X_train, X_train, epochs=10,
                      validation_data=(X_valid, X_valid))

# 가중치를 묶지 않은 인코더 loss
'''
Epoch 10/10
1719/1719 [==============================] - 8s 4ms/step - loss: 0.2847 - val_loss: 0.2866
'''

# 가중치를 묶은 인코더 loss
'''
Epoch 10/10
1719/1719 [==============================] - 7s 4ms/step - loss: 0.2817 - val_loss: 0.2827
'''

 

약간 더 낮은 재구성 오차를 달성합니다.

 

 

한 번에 오토인코더 한 개씩 훈련하기

 

탐욕적 방식의 층별 훈련(greedy layerwise training)은 오토인코더 하나를 훈련하고 이를 쌓아올려서 한 개의 적층 오토인코더를 만드는 방식입니다.

 

 

  • 훈련 단계 1에서 첫 번째 오토인코더는 입력을 재구성하도록 학습됩니다. 이 오토인코더를 이용해 전체 훈련 세트를 인코딩해서 압축된 새로운 훈련 세트를 만듭니다.
  • 훈련 단계 2에서 앞서 인코딩한 훈련 세트를 이용해 두 번째 오토인코더를 훈련하빈다.
  • 마지막으로 모든 인코더를 사용해 전체 네트워크를 만듭니다.

2006년 제프리 힌턴이 탐욕적인 층별 접근 방법을 사용하여 DNN을 비지도 형태로 사전훈련될 수 있다고 밝혔습니다.(https://homl.info/136) 이를 위해 제한된 볼츠만 머신(restricted boltszmann machine, RBM)을 사용했습니다. 이후 이것이 오토인코더에도 적용된다는 것이 밝혀졌습니다.

 

만들 오토인코더 구조

 

위 그림을 greedy layerwise training 방식으로 훈련해보겠습니다.

 

def train_autoencoder(n_neurons, X_train, X_valid, loss, optimizer,
                      n_epochs=10, output_activation=None, metrics=None):
  n_inputs = X_train.shape[-1]
  encoder = keras.models.Sequential([
            keras.layers.Dense(n_neurons, activation='selu', input_shape=[n_inputs])
  ])
  decoder = keras.models.Sequential([
            keras.layers.Dense(n_inputs, activation=output_activation)
  ])
  autoencoder = keras.models.Sequential([encoder, decoder])
  autoencoder.compile(optimizer, loss, metrics=metrics)
  autoencoder.fit(X_train, X_train, epochs=n_epochs,
                  validation_data = (X_valid, X_valid))
  return encoder, decoder, encoder(X_train), encoder(X_valid)
  # encoder, decoder, 인코딩된 train, 인코딩된 valid data
 
def rounded_accuracy(y_true, y_pred):
    return keras.metrics.binary_accuracy(tf.round(y_true), tf.round(y_pred))
tf.random.set_seed(42)
np.random.seed(42)

K = keras.backend
X_train_flat = K.batch_flatten(X_train) # shape = (55000, 784), X_train.shape = (55000, 28, 28)
X_valid_flat = K.batch_flatten(X_valid) # equivalent to .reshape(-1, 28*28)
enc1, dec1, X_train_enc1, X_valid_enc1 = train_autoencoder(
    100, X_train_flat, X_valid_flat, 'binary_crossentropy',
    keras.optimizers.SGD(lr=1.5), output_activation='sigmoid',
    metrics=[rounded_accuracy]
)

# X_train_enc1.shape = (55000, 100)

enc2, dec2, _, _ = train_autoencoder(
    30, X_train_enc1, X_valid_enc1, 'mse', keras.optimizers.SGD(lr=0.05),
    output_activation='selu'
)
stacked_ae_1_by_1 = keras.models.Sequential([
        keras.layers.Flatten(input_shape=[28, 28]),
        enc1, enc2, dec2, dec1,
# 784 → 100 → 30 → 100 → 784
        keras.layers.Reshape([28, 28])
])
# 훈련 전 상태의 결과
show_reconstructions(stacked_ae_1_by_1)
plt.show()

훈련 전 결과

 

이런 방식으로 더 많은 오토인코더를 훈련해 아주 깊은 적층 오토인코더를 만들 수 있습니다.

 

stacked_ae_1_by_1.compile(loss='binary_crossentropy',
                          optimizer=keras.optimizers.SGD(lr=0.1), metrics=[rounded_accuracy])
history = stacked_ae_1_by_1.fit(X_train, X_train, epochs=10,
                                validation_data=(X_valid, X_valid))

훈련 후 결과, 별 차이 없는 것 같다.

 

오토인코더는 밀집 네트워크에 국한되지 않고 합성곱 오토인코더나 순환 오토인코더도 만들 수 있습니다.

 

 

합성곱 오토인코더

 

이미지를 다룰 경우 (이미지가 큰 경우) 오토인코더는 좋은 성능을 내지 못합니다. 이미지는 CNN이 밀집 네트워크보다 더 좋은 성능을 내므로 합성곱 오토인코더(convolutional autoencoder)를 만들어야 합니다.

 

합성곱 오토인코더에서 인코더는 합성곱 층과 풀링 층으로 구성된 일반적인 CNN인데, 인코더는 전형적으로 입력에서 공간 방향의 차원을 줄이고 깊이(특성맵 수)를 늘립니다. 디코더는 거꾸로 이미지 스케일을 늘리고 깊이를 원본 차원으로 되돌립니다. 이를 위해 디코더는 전치 합성곱 층을 사용합니다. 혹은 합성곱 층과 업샘플링 층을 사용할 수도 있습니다.

 

conv_encoder = keras.models.Sequential([
        keras.layers.Reshape([28, 28, 1], input_shape=[28, 28]),
        # 깊이 차원을 늘려간다.
        keras.layers.Conv2D(16, kernel_size=3, padding='same', activation='selu'),
        # 깊이 차원마다 수용장 영억 내 가장 큰 값 1개만 pooling 한다.
        keras.layers.MaxPool2D(pool_size=2),
        keras.layers.Conv2D(32, kernel_size=3, padding='same', activation='selu'),
        keras.layers.MaxPool2D(pool_size=2),
        keras.layers.Conv2D(64, kernel_size=4, padding='same', activation='selu'),
        keras.layers.MaxPool2D(pool_size=2)
])
conv_decoder = keras.models.Sequential([
        keras.layers.Conv2DTranspose(32, kernel_size=3, strides=2, padding='valid',
                                     activation='selu',
                                     input_shape=[3, 3, 64]),
        keras.layers.Conv2DTranspose(16, kernel_size=3, strides=2, padding='same',
                                     activation='selu'),
        keras.layers.Conv2DTranspose(1, kernel_size=3, strides=2, padding='same',
                                     activation='sigmoid'),
        keras.layers.Reshape([28, 28])
])
conv_ae = keras.models.Sequential([conv_encoder, conv_decoder])
conv_ae.compile(loss='binary_crossentropy', optimizer=keras.optimizers.SGD(lr=1.0),
                metrics=[rounded_accuracy])
conv_ae.fit(X_train, X_train, epochs=5,
            validation_data=(X_valid, X_valid))

 

MaxPool2D에서 stride=None일 경우

x = tf.constant([
          [1., 2., 3.],
          [4., 5., 6.],
          [7., 8., 9.]
])
x = tf.reshape(x, [1, 3, 3, 1])
max_pool_2d = tf.keras.layers.MaxPool2D(pool_size=2)
max_pool_2d(x)
# 결과
'''
<tf.Tensor: shape=(1, 1, 1, 1), dtype=float32, numpy=array([[[[5.]]]], dtype=float32)>
'''
show_reconstructions(conv_ae)
plt.show()

 

 

순환 오토인코더

 

시계열, 텍스트와 같은 시퀀스를 비지도 학습, 차원 축소하기 위해 오토 인코더를 만들려면, 순환 신경망으로 오토인코더를 만드는 것이 낫습니다.

 

순환 오토인코더(recurrent autoencoder)는 쉽게 만들 수 있습니다. 인코더는 입력 시퀀스를 하나의 벡터로 압축하는 seq2vec RNN입니다. 디코더는 반대로 vec2seq RNN입니다.

 

recurrent_encoder = keras.models.Sequential([
        keras.layers.LSTM(100, return_sequences=True, input_shape=[None, 28]),
        keras.layers.LSTM(30)   # 왜 time sequence마다 출력하지 않는거지?
])
recurrent_decoder = keras.models.Sequential([
        keras.layers.RepeatVector(28, input_shape=[30]),
        keras.layers.LSTM(100, return_sequences=True),
        keras.layers.TimeDistributed(keras.layers.Dense(28, activation='sigmoid'))
])
recurrent_ae= keras.models.Sequential([recurrent_encoder, recurrent_decoder])

https://live-with-wisdom.tistory.com/45에서 LSTM autoencoder를 정리해놨음. 

recurrent_ae.compile(loss='binary_crossentropy', optimizer=keras.optimizers.SGD(lr=0.1))
history = recurrent_ae.fit(X_train, X_train, epochs=10, validation_data=(X_valid, X_valid))

 

시간 없어서 4 epochs에서 끊었습니다. 왼쪽이 교재의 그림

 

이 순환 오토인코더는 타임 스텝마다 28차원을 갖는 어떤 길이의 시퀀스를 처리할 수 있습니다. 여기서는 각 이미지를 행의 시퀀스로 간주하여 각 타임 스텝에서 이 RNN은 28픽셀의 행 하나를 처리합니다.

 

타임 스텝마다 입력 벡터를 주입하기 위해 디코더의 첫 번째 층에 RepeatVector 층 사용해 압축된 벡터를 timesteps 만큼 복사해 사용한 점에 주목하세요.

 

입력보다 큰 코딩 층을 두어 과대완전 오토인코더(overcomplete autoencoder)를 만들 수도 있습니다. 지금부터 이런 방법 중 몇 가지를 알아보겠습니다.

 

 

잡음 제거 오토인코더

 

오토인코더가 유용한 특성을 학습하도록 강제하는 방법에는 입력에 잡음을 추가하는 것이 있었습니다. 오토인코더를 특성 추출기로 사용할 수 있다는 논문도 나왔고, 파스칼 빈센트는 2010년 적층 잡음 제거 오토인코더(stacked denoising autoencoder)를 소개했습니다.

 

잡읍은 입력에 가우시안 잡음을 추가하거나 dropout을 사용해 입력을 무작위로 끌 수 있습니다.

 

 

구현은 간단합니다. Dropout 층을 이용해 사용했지만, GaussianNoise층을 사용해서도 적층 오토인코더를 만들 수 있습니다. Dropout 층과 GaussianNoise 층은 훈련하는 동안에만 활성화됩니다.

 

dropout_encoder = keras.models.Sequential([
      keras.layers.Flatten(input_shape=[28, 28]),
      keras.layers.Dropout(0.5),
      keras.layers.Dense(100, activation='selu'),
      keras.layers.Dense(30, activation='selu')
])
dropout_decoder = keras.models.Sequential([
      keras.layers.Dense(100, activation='selu',
                         input_shape=[30]),
      keras.layers.Dense(28*28, activation='sigmoid'),
      keras.layers.Reshape([28, 28])
])
dropout_ae = keras.models.Sequential([dropout_encoder, dropout_decoder])

dropout_ae.compile(loss='binary_crossentropy', optimizer=keras.optimizers.SGD(lr=1.0))
dropout_ae.fit(X_train, X_train, epochs=10, validation_data=(X_valid, X_valid))
def show_reconstructions(model, n_images=5, X_valid=X_valid):
  reconstructions = model.predict(X_valid[:n_images])
  fig = plt.figure(figsize=(n_images * 1.5, 3))
  for image_index in range(n_images):
    plt.subplot(2, n_images, 1+image_index)
    plot_image(X_valid[image_index])
    plt.subplot(2, n_images, 1 + n_images + image_index)
    plot_image(reconstructions[image_index])
dropout = keras.layers.Dropout(0.5)
show_reconstructions(dropout_ae, X_valid=dropout(X_valid, training=True))
plt.show()

 

4번째 티셔츠의 경우 입력에 없는 정보를  모델이 어떻게 추측하는지 주목하세요. 간단하고 효율적으로 이미지에서 잡음을 제거하는데 오토인코더를 사용할 수 있습니다.

 

 

희소 오토인코더

 

오토인코더를 제약하는 또 다른 방식에는 희소(sparsity)가 있습니다. 비용 함수에 적절한 항을 추가해 오토인코더의 코딩층에서 활성화되는 뉴런 수를 감소시킬 수 있습니다. 예를 들어 평균적으로 5%의 뉴런만 활성화되도록 강제해서 오토인코더가 더 적은 수의 활성화된 뉴런을 조합하여 입력을 표현해야 합니다.

 

간단한 방법은 코딩 층에 sigmoid 활성화 함수를 사용해서 코딩을 0~1의 값으로 제한하고, 큰 코딩층(예를 들어, 300개의 유닛을 가진)을 사용하는 것입니다. 코딩 층의 활성화 값에 $l_1$ 규제를 추가합니다.

 

디코더는 일반적인 디코더입니다.

sparse_l1_encoder = keras.models.Sequential([
      keras.layers.Flatten(input_shape=[28, 28]),
      keras.layers.Dense(100, activation='selu'),
      keras.layers.Dense(300, activation='sigmoid'),
      keras.layers.ActivityRegularization(l1=1e-3)
])
sparse_l1_decoder = keras.models.Sequential([
      keras.layers.Dense(100, activation='selu', input_shape=[300]),
      keras.layers.Dense(28*28, activation='sigmoid'),
      keras.layers.Reshape([28, 28])
])
sparse_l1_ae = keras.models.Sequential([sparse_l1_encoder, sparse_l1_decoder])

 

ActivityRegularization층은 입력을 그대로 반환하면서 훈련 손실에 입력의 절댓값의 합을 더합니다. 이 층을 제거하고 이전 층에 activity_regularizer=keras.regularizers.l1(1e-3)으로 지정해도 됩니다. $l_1$ 규제는 약한 입력을 없애는 특징을 가져서 입력 이미지에서 불필요한 것을 제거합니다.

 

sparse_l1_ae

 

또 다른 방법은 훈련 반복마다 코딩 층의 희소 정도타깃의 희소 정도와 비교하는 것입니다. 이를 위해 훈련 배치에 대해 코딩 층에 있는 각 뉴런의 평균적인 활성화 정도를 계산합니다. 배치 크기는 너무 작지 않아야 합니다. 그렇지 않으면 평균값이 정확하지 않을 수 있습니다.

 

각 뉴런에 대한 활성화 정도를 이용해 희소 손실(sparsity loss)을 비용함수에 추가해서 너무 활성화되거나 충분히 활성화되지 않은 뉴런에 벌칙을 가할 수 있습니다. 예를 들어, 한 뉴런의 활성화가 0.3, 목표 희소 정도가 0.1이라면 비용 함수에 MSE인 $(0.3-0.1)^2$을 추가하는 식입니다. 실전에서는 MSE보다 더 좋은 gradient를 가져서 빠르게 수렴하도록 돕는 쿨백-라이블러 발산(Kullback-Leibler divergence)을 사용합니다.

 

 

두 개의 이산 확률 분포 $P$, $Q$가 주어졌을 때, 이 두 분산 사이의 KL 발산 $D_{KL}\left(P\parallel Q\right)$은 아래 식과 같습니다.

 

[Kullback-Leibler divergence]

$$D_{KL}\left(P\parallel Q\right)=\sum _i^{\ }P\left(i\right)\log \frac{P\left(i\right)}{Q\left(i\right)}$$

 

여기에서는 코딩 층에서 뉴런이 활성화될 목표 확률 $p$와 실제 확률$q$(훈련 배치에 대한 평균 활성화) 사이의 KL 발산을 측정합니다.

 

[목표 희소 정도 p, 실제 희소 정도 q 사이의 KL 발산]

$$D_{KL}\left(p\parallel q\right)=p\log \frac{p}{q}+\left(1-p\right)\log \frac{1-p}{1-q}$$

 

각 뉴런에 대해 활성화됐을 때, 그렇지 않을 때 두 가지 경우만 있으므로 KL발산은 위 식과 같아집니다. $p, q$가 0~1사이 일 때, $p, q$보다 작아지지도 않고 $p, q$보다 커지지도 않게끔 해주는 것 같다.

 

코딩 층의 각 뉴런에 대한 손실을 모두 합해서 비용 함수에 더합니다. 희소 손실과 재구성 손실의 상대적 중요도를 조절하기 위해 희소 손실에 가중치 하이퍼파라미터를 곱합니다. 희소 가중치가 크면 p와 q를 거의 같게 만들어 비용 함수를 줄이도록 학습할 것입니다. 희소 가중치가 너무 크면 적절히 재구성을 하지 못할 수도 있습니다.

 

KL 발산 규제를 사용자 정의 규제로 구현해보겠습니다.

K = keras.backend
kl_divergence = keras.losses.kullback_leibler_divergence

class KLDivergenceRegularizer(keras.regularizers.Regularizer):
  def __init__(self, weight, target=0.1):
    self.weight=weight
    self.target=target
  def __call__(self, inputs):
    mean_activities = K.mean(inputs, axis=0)
    return self.weight*(
        kl_divergence(self.target, mean_activities) + 
        kl_divergence(1.- self.target, 1. - mean_activities)
    )

 

코딩 층의 활성화에 KLDivergenceRegularizer를 적용해 희소 오토인코더를 만듭니다.

 

kld_reg = KLDivergenceRegularizer(weight=0.05, target=0.1)
sparse_kl_encoder = keras.models.Sequential([
        keras.layers.Flatten(input_shape=[28, 28]),
        keras.layers.Dense(100, activation='selu'),
        keras.layers.Dense(300, activation='sigmoid',
                           activity_regularizer=kld_reg)
])
sparse_kl_decoder = keras.models.Sequential([
        keras.layers.Dense(100, activation='selu', input_shape=[300]),
        keras.layers.Dense(28*28, activation='sigmoid'),
        keras.layers.Reshape([28, 28])
])
sparse_kl_ae = keras.models.Sequential([sparse_kl_encoder, sparse_kl_decoder])
sparse_kl_ae.compile(loss='binary_crossentropy', optimizer=keras.optimizers.SGD(lr=1.))
history = sparse_kl_ae.fit(X_train, X_train, epochs=10,
                           validation_data=(X_valid, X_valid))
show_reconstructions(sparse_kl_ae)

def plot_percent_hist(ax, data, bins):
  counts, _ = np.histogram(data, bins=bins)
  # np.histogram([1, 2, 1], bins=[0, 1, 2, 3])
  # >>> (array([0, 2, 1]), array([0, 1, 2, 3])) = [0, 1) 0개, [1, 2) 2개, [2, 3] 1개
  widths = bins[1:] - bins[:-1]
  # 아래 값들은 ax1에서 계산한 값들입니다.
  # widths = array([0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1])
  x = bins[:-1] + widths / 2
  # x = array([0.05, 0.15, 0.25, 0.35, 0.45, 0.55, 0.65, 0.75, 0.85, 0.95])
  ax.bar(x, counts/len(data), width=widths*0.8)
  # x.shape = (10,), counts.shape = (10,)
  ax.xaxis.set_ticks(bins)
  ax.yaxis.set_major_formatter(mpl.ticker.FuncFormatter(
      lambda y, position: "{}%".format(int(np.round(100*y)))
  ))
  ax.grid(True)
  
def plot_activations_histogram(encoder, height=1, n_bins=10):
  X_valid_codings = encoder(X_valid).numpy()
  activation_means = X_valid_codings.mean(axis=0)
  mean = activation_means.mean()
  bins = np.linspace(0, 1, n_bins + 1)

  fig, [ax1, ax2] = plt.subplots(figsize=(10, 3), nrows=1, ncols=2, sharey=True)
  plot_percent_hist(ax1, X_valid_codings.ravel(), bins) # ravel()은 1차원으로 피는 것
  ax1.plot([mean, mean], [0, height], 'k--', label='Overall Mean = {:.2f}'.format(mean)) 
  ax1.legend(loc='upper center', fontsize=14)
  ax1.set_xlabel('Activation')
  ax1.set_ylabel('% Activations')
  ax1.axis([0, 1, 0, height]) # axis([xmin, xmax, ymin, ymax])
  plot_percent_hist(ax2, activation_means, bins)
  ax2.plot([mean, mean], [0, height], 'k--') # 가운데 점선
  ax2.set_xlabel("Neuron Mean Activation")
  ax2.set_ylabel('% Neurons')
  ax2.axis([0, 1, 0, height])
plot_activations_histogram(sparse_kl_encoder)
plt.show()

코딩 층에 있는 모든 활성화 분포(왼쪽)와 각 뉴런의 평균 활성화 분포(오른쪽)

Fashion MNIST 희소 인코더를 훈련한 후 코딩층의 뉴런의 활성화가 거의 0에 가까워졌습니다. 전체 활성화량의 약 70%가 0.1 보다 작은 활성화입니다. 전체 뉴런의 약 90%가 0.1 ~ 0.2 사이의 평균 활성화를 가집니다.?

 

 

변이형 오토인코더

 

변이형 오토인코더와 지금까지 다룬 오토인코더의 차이점이 있습니다.

  • 확률적 오토인코더(probabilistic autoencoder)입니다. 즉, 훈련이 끝난 후에도 출력이 부분적으로 우연에 의해 결정됩니다. 이와는 반대로 잡음 제거 오토인코더는 훈련 시에만 무작위성을 사용합니다.
  • 생성 오토인코더(generative autoencoder)입니다. 훈련 세트와 비슷한 새로운 샘플을 생성합니다.

위와 같은 두 속성이 변이형 오토인코더를 RBM과 유사하게 만듭니다. 변이형 오토인코더는 효율적인 근사 베이즈 추론 방법인 변분 베이즈 추론(variational Bayesian inference)을 수행합니다.

 

 

변이형 오토인코더는 주어진 입력에 대해 바로 코딩을 만들지 않고, 평균 코딩 $\mu $과 표준편차 $\sigma $를 만듭니다. 실제 코딩은 평균이 $\mu $이고, 표준편차가 $\sigma $인 가우시안 분포에서 랜덤하게 샘플링됩니다. 그 다음 이 코딩이 디코딩되어 훈련 샘플을 닮은 출력을 만듭니다.

 

그림에서 볼 수 있듯이 변이형 오토인코더는 복잡한 분포의 입력도 훈련하는 동안 비용 함수를 통해 코딩을 가우시안 분포를 가진 군집으로 만들면서 코딩 공간(coding space, latent space) 안으로 이동시킵니다. 훈련이 끝난 후 단지 가우시안 분포에서 랜덤한 코딩을 샘플링해 디코딩하면 됩니다. 가우시안 잡음은 전달하는 정보의 양을 제한해 유용한 특성을 학습하게 만듭니다.

 

변이형 오토인코더의 비용함수는 두 부분으로 구성됩니다.

  1. 첫 번째는 일반적인 재구성 손실입니다. 앞서 사용한 것처럼 크로스 엔트로피를 사용할 수 있습니다.
  2. 두 번째는 코딩을 가우시안 분포로 만들도록 강제하는 잠재 손실(latent loss)입니다. 실제 코딩 분포와 목표 분포(가우시안 분포) 사이에 KL 발산을 사용합니다.

 

[변이형 오토인코더 잠재 손실]

$$L =-\frac{1}{2}\sum _{i=1}^n\left(1+\log \left(δ_i^2\right)-δ_i^2-μ_i^2\right)$$

 

여기서 n은 코딩의 차원, $μ_i$, $δ_i$는 $i$번째 코딩 원소의 평균과 표준편차입니다. 모든 $μ_i$, $δ_i$를 포함하는 벡터 $μ$와 벡터 $δ$는 [그림 17-12]의 인코더의 출력입니다. (수학적인 상세한 내용은 https://homl.info/116을 참고하세요.)

 

실제로 많이 $δ$를 $\gamma =\log \left(δ^2\right)$로 대체합니다. 그럼 잠재 손실은 아래와 같습니다.

 

[대체된 변이형 오토인코더 잠재 손실]

$$\gamma =\log \left(δ^2\right)$$

$$L =-\frac{1}{2}\sum _{i=1}^n\left(1+\gamma _i-\exp \left(\gamma _i\right)-μ_i^2\right)$$

 

이 방식이 수학적으로 안정적이고 훈련 속도를 높입니다.

 

패션 MNIST 데이터셋에서 변이형 오토인코더를 만들어보겠습니다. 먼저, $μ$와 $\gamma $가 주어졌을 때 코딩을 샘플링하는 사용자 정의 층을 정의하겠습니다.

class Sampling(keras.layers.Layer):
  def call(self, inputs):
    mean, log_var = inputs
    return K.random_normal(tf.shape(log_var))*K.exp(log_var/2) + mean

K.random_normal() 함수를 사용해 평균이 0이고, 표준편차가 1인 정규분포에서 $\gamma $와 동일한 크기의 랜덤한 벡터를 샘플링합니다. $δ$를 반환하도록 $exp(\gamma / 2)$를 곱하고 마지막으로 $μ$를 더해줍니다. 결과적으로 평균이 $μ$이고 표준편차가 $δ$인 정규분포에서 샘플링한 코딩 벡터를 반환합니다.

 

이 모델은 완전히 순차적이지 않기 때문에 함수형 API를 사용해 인코더를 만듭니다.

codings_size = 10

inputs = keras.layers.Input(shape=[28, 28])
z = keras.layers.Flatten()(inputs)
z = keras.layers.Dense(150, activation='selu')(z)
z = keras.layers.Dense(100, activation='selu')(z)
codings_mean = keras.layers.Dense(codings_size)(z) # μ, 그냥 평균을 이렇게 만드는 구나
codings_log_var = keras.layers.Dense(codings_size)(z) # γ
codings = Sampling()([codings_mean, codings_log_var])
variational_encoder = keras.Model(
    inputs=[inputs], outputs=[codings_mean, codings_log_var, codings]
)

실제 사용하는 것은 마지막  출력(codings)입니다. 디코더를 만들어보겠습니다. 디코더는 Sequential 모델로 만들어도 됩니다.

# Functional API
decoder_inputs = keras.layers.Input(shape=[codings_size])
x = keras.layers.Dense(100, activation='selu')(decoder_inputs)
x = keras.layers.Dense(150, activation='selu')(x)
x = keras.layers.Dense(28*28, activation='sigmoid')(x)
outputs = keras.layers.Reshape([28, 28])(x)
variational_decoder = keras.Model(inputs=[decoder_inputs], outputs=[outputs])


# Sequential API
variational_decoder = keras.models.Sequential([
      keras.layers.Dense(100, activation='selu', input_shape=[codings_size]),
      keras.layers.Dense(150, activation='selu'),
      keras.layers.Dense(28*28, activation='sigmoid'),
      keras.layers.Reshape([28, 28])
])

마지막으로 인코더와 디코더를 합쳐줍니다.

_, _, codings = variational_encoder(inputs)
reconstructions = variational_decoder(codings)
variational_ae = keras.Model(inputs=[inputs], outputs=[reconstructions])

마지막으로 잠재 손실을 비용함수에 추가합니다.

latent_loss = -0.5*K.sum(
    1 + codings_log_var - K.exp(codings_log_var) - K.square(codings_mean), axis=-1
)
variational_ae.add_loss(K.mean(latent_loss)/784.) 
# 784는 정규화 ? > 재구성 손실과 비교했을 때 적절한 크기로 만들어주기 위해
variational_ae.compile(loss='binary_crossentropy', optimizer='rmsprop')

latent_loss는 배치에 있는 각 샘플의 잠재 손실을 계산하고 모두 더한 것입니다. 그 다음 평균을 계산하고 재구성 손실에 비례해 적절한 크기가 되도록 784를 나눕니다.

 

실제로 변이형 오토인코더의 재구성 손실은 픽셀마다 재구성 오차의 합이지만, 케라스의 binary_crossentropy 손실을 계산할 때 전체 픽셀의 평균을 계산합니다. 따라서 재구성 손실이 원래보다 784 배 작습니다. 평균이 아닌 합을 계산하는 사용자 정의 함수를 사용할 수도 있지만, 편의상 잠재 손실을 784로 나누었습니다. 이는 최종 손실이 원래보다 784배 작아진다는 뜻이므로 더 큰 학습률을 사용해 보상해줘야합니다.

 

Fashion MNIST 문제에서는 RMSprop 옵티마이저가 잘 맞습니다.

history = variational_ae.fit(X_train, X_train, epochs=50, batch_size=128,
                             validation_data = (X_valid, X_valid))

 

패선 MNIST 이미지 생성하기

 

변이형 오토인코더를 사용해 패션 의류처럼 보이는 이미지를 생성해보겠습니다. 해야 할 일은 가우시안 분포에서 랜덤한 코딩을 샘플링하여 디코딩하는 것이 전부입니다.

 

codings = tf.random.normal(shape=[12, codings_size])
images = variational_decoder(codings).numpy() # images.shape = (12, 28, 28)

fig = plt.figure(figsize=(12*1, 4.5))
for i in range(12):
  plt.subplot(3, 4, i+1)
  plt.imshow(images[i, :28, :28], cmap='binary')
  plt.axis('off')

변이형 오토 인코더가 생성한 이미지 !!

 

단 몇 분만 학습하고 그린 이미지입니다. 세부 튜닝을 하고, 오랫동안 훈련하면 훨씬 좋은 이미지가 만들어집니다.

 

변이형 오토인코더는 시맨틱 보간(semantic interpolation)을 수행할 수 있습니다. 보간은 두 이미지가 겹쳐보이게끔 만드는 것입니다. 픽셀 수준의 보간 대신 코딩 수준에서 두 이미지를 보간할 수 있습니다.

 

codings_grid = tf.reshape(codings, [1, 3, 4, codings_size])
# 12개의 코딩을 3x4 격자로 만듦

larger_grid = tf.image.resize(codings_grid, size=[5, 7]) 
# 격자 크기를 5x7로 바꿈, TensorShape([1, 5, 7, 10]), 여기서 데이터가 증식되는 듯

 

기본적으로 tf.image.resize() 함수는 이중 선형 보간(bilinear interpolation)을 수행합니다. 따라서 모든 행과 열에서 보간된 코딩을 가집니다. 그 다음 디코더로 이미지를 생성합니다. resize() 함수의 method 매개변수로 다른 보간 방법을 선택할 수 있습니다. 이중 선형 보간은 1차원 선형 보간을 2차원으로 확장한 것입니다.?

 

interpolated_codings = tf.reshape(larger_grid, [-1, codings_size])
images = variational_decoder(interpolated_codings).numpy()
plt.figure(figsize=(7, 5))
for index, image in enumerate(images):
    plt.subplot(5, 7, index + 1)
    if index%7%2==0 and index//7%2==0:  
      # index가 짝수 행(index//7%2) & 짝수 열(index%7%2)
        plt.gca().get_xaxis().set_visible(False) # 눈금 축, 라벨만 제거
        plt.gca().get_yaxis().set_visible(False)
    else:
        plt.axis("off") # 전체 다 제거인듯?
    plt.imshow(image, cmap="binary")

원본 이미지는 태두리가 있는 것들이며, 나머지는 근방의 이미지를 시맨틱 보간했습니다. 가령, (행, 열)이 (4, 5)인 바지와 신발을 보간한 것입니다.

 

지난 몇 년간은 오토인코더가 널리 쓰였지만 GAN이 훨씬 또련한 이미지를 만들어서 인기가 더 높습니다. 이제 GAN을 알아보겠습니다.

 

 

생성적 적대 신경망

 

GAN은 Ian Goodfellow 등이 2014년에 제안했지만, GAN 훈련의 어려움을 극복하는데 몇 년이 걸렸습니다. GAN은 아래 그림처럼 생성자판별자라는 신경망 두 개로 구성됩니다.

 

 

생성자

 

랜덤한 분포(일반적으로 가우시안 분포)를 입력으로 받아서 이미지와 같은 데이터를 출력합니다. 랜덤한 입력은 생성할 이미지의 잠재 표현(즉, 코딩)으로 생각할 수 있습니다. 변이형 오토인코더의 디코더와 같은 방식으로 가우시안 잡음을 주입하여 완전히 새로운 이미지를 출력합니다. 하지만, 훈련 방식은 매우 다릅니다.

 

판별자

 

생성자에서 얻은 가짜 이미지나 훈련 세트에서 추출한 진짜 이미지를 입력으로 받아 입력된 이미지가 진짜인지 가짜인지 구분합니다.

 

훈련하는 동안 생성자는 진짜 이미지와 가짜 이미지를 구분하고, 생성자는 판별자를 속일 만큼 진짜 같은 이미지를 만듭니다. GAN은 다른 목표를 가진 두 네트워크로 구성되므로 일반적인 신경망과는 다른 방법으로 훈련합니다. 훈련 반복은 두 단계로 구성됩니다.

 

  • 첫 번째 단계에서는 판별자를 훈련합니다. 훈련 세트에서 진짜 이미지의 배치를 샘플링하고, 생성자에서 만든 동일한 수의 가짜 이미지와 합쳐 하나의 데이터셋을 만듭니다. 그리고 가짜 이미지는 0으로 레이블링하고 진짜 이미지는 1로 레이블링합니다. 판별자는 binary_crossentropy를 사용해 한 스텝 동안 이렇게 레이블된 배치로 훈련합니다. 이 단계에서 역전파는 판별자의 가중치만을 최적화합니다.
  • 두 번째 단계에서는 생성자를 훈련합니다. 먼저 생성자로 가짜 이미지 배치를 만들고 다시 판별자를 사용해 이미지가 진짜인지 가짜인지 판별합니다. 이번엔 배치에 진짜 이미지를 추가하지 않고 레이블을 모두 1로 세팅합니다. 다른 말로 하면 판별자가 진짜라고 믿을 이미지를 만들어야합니다. (판별자가 1이라고 판단해야 정답과 같아지니깐 loss가 작아지는 듯) 이 단계에서는 판별자의 가중치를 동결합니다. 따라서 생성자의 가중치에만 영향을 미칩니다.

NOTE_

더보기

생성자는 실제 진짜 이미지를 입력받지 않고, 점진적으로 진짜라고 믿을 만한 가짜 이미지를 만드는 법을 배웁니다. 생성자가 입력받는 것은 판별자를 통해 전달되는 그레디언트가 전부입니다. 다행히 판별자의 성능이 좋아질 수록 이 간접 그레디언트에 진짜 이미지의 정보가 많이 담깁니다. 따라서 생성자의 성능이 크게 향상될 수 있습니다.

 

 

패션 MNIST 데이터셋으로 간단한 GAN을 만들어보겠습니다.

 

생성자와 판별기를 만들겠습니다. 생성자는 오토인코더의 디코더와 비슷하고, 판별자는 이미지를 입력받고 하나의 유닛과 시그모이드 활성화 함수를 사용한 Dense 층으로 끝나는 일반적인 이진 분류기입니다. 

 

codings_size = 30

generator = keras.models.Sequential([
      keras.layers.Dense(100, activation='selu', input_shape=[codings_size]),
      keras.layers.Dense(150, activation='selu'),
      keras.layers.Dense(28*28, activation='sigmoid'),
      keras.layers.Reshape([28, 28])
])
discriminator = keras.models.Sequential([
      keras.layers.Flatten(input_shape=[28, 28]),
      keras.layers.Dense(150, activation='selu'),
      keras.layers.Dense(100, activation='selu'),
      keras.layers.Dense(1, activation='sigmoid')
])
gan = keras.models.Sequential([generator, discriminator])

 

판별자와 GAN 모델 모두 이진 분류기이기 때문에 loss로 binary_crossentropy를 사용합니다. 중요한 것은 앞에서 말한 훈련의 두 번째 단계에서 판별자를 훈련하면 안된다는 것입니다. 따라서 gan 모델을 컴파일하기 전에 판별자가 훈련되지 않도록 설정해야합니다.?

 

discriminator.compile(loss='binary_crossentropy', optimizer='rmsprop')
discriminator.trainable = False
gan.compile(loss='binary_crossentropy', optimizer='rmsprop')

 

NOTE_

더보기

trainable 속성은 모델을 컴파일할 때만 영향을 줍니다. 위에 있는 코드를 실행하면 discriminator의 fit() 메서드와 앞으로 사용할 train_on_batch() 메서드를 호출해도 판별자는 여전히 훈련됩니다. 반면 gan 모델의 메서드를 호출할 때는 훈련되지 않습니다.

 

훈련이 일반적인 반복이 아니기 때문에 사용자 정의 훈련 반복문을 만들겠습니다. 이를 위해 먼저 이미지를 순회하는 Dataset을 만들어야합니다.

 

batch_size=32
# X_train.shape=(55000, 28, 28)을 (28, 28) 텐서 55000개로 나눈다.
dataset = tf.data.Dataset.from_tensor_slices(X_train).shuffle(1000)
# batch_size만큼 담긴 데이터셋으로 나눈다. (32, 28, 28) 텐서 1718개로 나눈다.
dataset = dataset.batch(batch_size, drop_remainder=True).prefetch(1)

 

def plot_multiple_images(images, n_cols=None):
  n_cols = n_cols or len(images)
  n_rows = (len(images) - 1)//n_cols + 1
  if images.shape[-1] == 1:
    images = np.squeeze(images, axis=-1) # 크기가 1인 축 제거
  plt.figure(figsize=(n_cols, n_rows))
  for index, image in enumerate(images):
    plt.subplot(n_rows, n_cols, index + 1)
    plt.imshow(image, cmap='binary')
    plt.axis('off')
    
# np.squeeze() 예시
'''
x = np.array([[[0], [1], [2]]])
x.shape
>> (1, 3, 1)
np.squeeze(x).shape
>> (3,)
np.squeeze(x, axis=0).shape
>> (3, 1)
np.squeeze(x, axis=1).shape
>> Error
'''

 

사용자 정의 훈련 반복문 train_gan()을 만들겠습니다.

 

def train_gan(gan, dataset, batch_size, codings_size, n_epochs=50):
  generator, discriminator = gan.layers # gan=keras.models.Sequential([generator, discriminator])
  for epoch in range(n_epochs):
    print("Epoch {}/{}".format(epoch + 1, n_epochs))
    for X_batch in dataset:
      # 1단계 - 판별자 훈련
      noise = tf.random.normal(shape=[batch_size, codings_size])
      generated_images = generator(noise)
      X_fake_and_real = tf.concat([generated_images, X_batch], axis=0)
      y1 = tf.constant([[0.]]*batch_size + [[1.]]*batch_size) # 정답 레이블
      # 컴파일시 trainable=True였지만, 현재는 False일 때(또는 반대시) 케라스의 경고를 제거합니다.
      discriminator.trainable=True 
      discriminator.train_on_batch(X_fake_and_real, y1)
      # 2단계 - 생성자 훈련
      noise = tf.random.normal(shape=[batch_size, codings_size])
      y2 = tf.constant([[1.]]*batch_size)
      # 컴파일시 trainable=False였지만, 현재는 True일 때(또는 반대시) 케라스의 경고를 제거합니다.
      discriminator.trainable = False
      gan.train_on_batch(noise, y2)
    plot_multiple_images(generated_images, 8)
    plt.show()
    
train_gan(gan, dataset, batch_size, codings_size) # colab 기준 31분 걸림

 

이게 전부입니다! 첫 번째 에포크가 끝난 후 패션 MNIST처럼 보이는 이미지를 볼 수 있습니다. 그런데, 잡음이 너무 많습니다. 안타깝지만 이보다 더 좋은 이미지는 생성되지 않는데, 이것은 GAN 훈련이 어렵다고 말하는 이유입니다.

 

noise = tf.random.normal(shape=[batch_size, codings_size])
images = generator(noise).numpy()

plt.figure(figsize=(7, 5))
for index, image in enumerate(images):
  plt.subplot(5, 7, index+1)
  plt.axis('off')
  plt.imshow(image, cmap='binary')

 

 

 

 

GAN 훈련의 어려움

 

내시 균형(Nash equilibrium)은 다른 플레이어가 전략을 수정하지 않을 것이므로 어떤 플레이어도 자신의 전략을 수정하지 않는 상태입니다. 예를 들어 모든 사람이 도로 왼쪽으로만 운전하거나, 포식자는 먹이를 쫓고 먹이감은 도망치는 것이 있습니다.

 

GAN은 하나의 내시 균형에만 도달할 수 있습니다. 생성자가 실제 이미지와 완벽하게 이미지를 만들어서 판별자가 (50%은 진짜, 50%는 가짜) 추측할 수 밖에 없을 때입니다. GAN을 충분히 오래 훈련한다고해도 이 균형에 도달할 것이라는 보장은 없습니다.

 

가장 큰 어려움은 모드 붕괴(mode collapse)입니다. 즉, 생성자의 출력의 다양성이 줄어들 때입니다. 생성자가 신발을 다른 클래스보다 그럴싸하게 만든다면, 생성자는 신발이 판별자를 속이기 쉽기 때문에 신발을 많이 만들도록 유도할 것이고, 점진적으로 다른 이미지를 생성하는 법을 잊게 됩니다. 판별자가 보게될 유일한 가짜 이미지는 신발이 될 것이고, 판별자도 다른 클래스의 가짜 이미지를 구별하는 방법을 잊어버릴 것입니다. 판별자가 가짜 신발을 구별하게 되면, 생성자는 셔츠로 옮겨가서 잘 훈련하면 또 신발에 대해서는 잊이버릴 것입니다. 이 과정이 반복되어서 어떤 클래스에서도 좋은 결과를 만들지 못할 수 있습니다.

 

생성자와 판별자는 서로에게 지속적으로 영향을 주기 때문에 파라미터 변동이 크고 불안정해질 수 있습니다. 안정적으로 훈련이 되다가도 이유 없이 갑자기 발산할 수도 있습니다. 이런 하이퍼파라미터를 세부 튜닝하기 위해 많은 노력이 필요합니다.

 

이러한 문제를 해결하기 위해 새로운 비용 함수를 제안하거나 (주요 GAN 손실을 잘 비교한 사이트가 있습니다.(https://homl.info/ganloss) ) 안정적으로 훈련해서 모드 붕괴를 피하기 위한 기법들이 제안됐습니다.

  • 경험 재생(experience replay), 매 반복에서 생성자가 만든 이미지를 재생 버퍼에 저장합니다. 재생 버퍼는 오래된 이미지는 점차 삭제해갑니다. 판별자를 훈련할 때 실제 이미지와 재생 버퍼에서 추출한 이미지를 사용합니다. 이는 판별자가 가장 최근 출력에 과대적합될 가능성을 줄입니다.
  • 미니배치 판별(mini-batch discrimination), 이 방법은 배치 간에 얼마나 비슷한 이미지가 있는지에 대한 통계를 판별자에게 제공합니다. 판별자는 다양성이 부족한 가짜 이미지 배치 전체를 쉽게 거부할 수 있습니다. 이는 생성자가 다양한 이미지를 생성하도록 유도하여 모드 붕괴의 위험을 줄여줍니다.

다른 논문들은 훈련이 잘 수행되는 특정한 네트워크 구조를 제안합니다.

 

GAN의 역학은 아직 완벽하게 파악하지 못했습니다. 가장 뛰어난 GAN 모델을 살펴보겠습니다. 몇 년전 최고 성능을 낸 심층 합성곱 GAN부터 시작해보겠습니다.

 

 

심층 합성곱 GAN

 

2014년에 발표된 원본 GAN 논문에서도 합성곱 층으로 실험했지만 작은 이미지만 생성했습니다. 2015년 큰 이미지를 위해 깊은 합성곱 층을 기반으로 한 심층 합성곱 GAN(deep convolutional GAN, DCGAN)을 발표했습니다. 다음은 안정적인 합성곱 GAN을 구축하기 위해 논문에서 제안한 주요 가이드 라인입니다.

  • 판별자에 있는 풀링 층을 스트라이드 합성곱으로 바꿉니다.
  • 생성자에 있는 풀링 층은 전치 합성곱으로 바꿉니다.
  • 생성자와 판별자에 배치 정규화를 사용합니다. (생성자의 출력 층과 판별자의 입력 층은 제외합니다.)
  • 층을 깊게 쌓기 위해 완전 연결 은닉층을 제거합니다.
  • tanh 함수를 사용해야하는 출력층을 제외하고 생성자의 모든 충은 ReLU 활성화 함수를 사용합니다.
  • 판별자의 모든 층은 LeakyReLU 활성화 함수를 사용합니다.

이 가이드라인은 항상 맞지는 않습니다. 따라서 여러 가지 하이퍼파라미터를 실험해봐야 합니다. 사실 random seed만 바꾸고 같은 모델을 훈련해도 이따금 성공합니다. 

 

다음 코드는 패션 MNIST에서 잘 동작하는 작은 DCGAN 모델입니다.

codings_size = 100

generator = keras.models.Sequential([
      keras.layers.Dense(7*7*128, input_shape=[codings_size]),
      keras.layers.Reshape([7, 7, 128]),
      keras.layers.BatchNormalization(),
      keras.layers.Conv2DTranspose(64, kernel_size=5, strides=2, padding='same',
                                   activation='selu'),
      keras.layers.BatchNormalization(),
      keras.layers.Conv2DTranspose(1, kernel_size=5, strides=2, padding='same',
                                   activation='tanh')
])
discriminator = keras.models.Sequential([
      keras.layers.Conv2D(64, kernel_size=5, strides=2, padding='same',
                          activation=keras.layers.LeakyReLU(0.2),
                          input_shape=[28, 28, 1]),
      keras.layers.Dropout(0.4),
      keras.layers.Conv2D(128, kernel_size=5, strides=2, padding='same',
                          activation=keras.layers.LeakyReLU(0.2)),
      keras.layers.Dropout(0.4),
      keras.layers.Flatten(),
      keras.layers.Dense(1, activation='sigmoid')
])
gan = keras.models.Sequential([generator, discriminator])

 

생성자는 크기 100의 코딩을 받아 6272(7*7*128) 차원으로 투영하고 이 결과를 7x7x128 크기의 텐서로 바꿉니다. 이 텐서는 배치 정규화를 거치고 stride=2인 전치 합성곱 층(넓이 차원이 2x2배가 됨)에 주입됩니다. 여기서 7x7x128 → 14x14x64로 변합니다. (넓이 차원은 업샘플링, 깊이는 다운 샘플링) 다시 배치 정규화 층을 지나고 stride=2인 전치 합성곱 층에 주입됩니다. 여기서 28x28x1로 변합니다. 이 층은 tanh 활성화 함수를 사용해 출력 범위가 -1 ~ 1 사이입니다. 때문에 GAN을 훈련하기 전에 훈련 세트를 동일한 범위로 스케일 조정해주어야합니다. 즉, 크기를 바꾸고 채널 차원을 추가해야합니다.

 

X_train = X_train.reshape(-1, 28, 28, 1)*2. -1. # shape, scale 변경
# X_train.shape (55000, 28, 28) → (55000, 28, 28, 1)
# X_train (min, max) = (0, 1) → (-1, 1)

 

판별자는 이진 분류를 위한 일반적인 CNN과 매우 비슷합니다. 다만, 다운 샘플링을 할 때 최대 풀링 층이 아닌 스트라이드 합성곱(stride=2)를 사용합니다. 또한 LeakyReLU 활성화 함수를 사용한다는 것도 눈여겨보세요.

# discriminator shape
'''
Model: "sequential_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d (Conv2D)             (None, 14, 14, 64)        1664      
                                                                 
 dropout (Dropout)           (None, 14, 14, 64)        0         
                                                                 
 conv2d_1 (Conv2D)           (None, 7, 7, 128)         204928    
                                                                 
 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
_________________________________________________________________
'''

 

전체적으로 DCGAN 가이드라인을 따랐지만 판별자에 BatchNormalization 층을 Dropout 층으로 대체했습니다. 그렇지 않으면 이 예제에서는 훈련이 불안정해집니다. 또한, 생성자에 ReLU 대신 SELU로 바꾸었습니다. 자류롭게 이 구조를 바꿔보면 하이퍼파라미터에 얼마나 민감한지 볼 수 있습니다. 특히, 두 네트워크의 상대적인 학습률이 그렇습니다.

 

마지막으로 앞에서 원본 gan에서 한 것처럼 데이터셋 , 컴파일하고 train_gan() 함수로 훈련합니다.

batch_size=32
dataset = tf.data.Dataset.from_tensor_slices(X_train).shuffle(1000)
dataset = dataset.batch(batch_size, drop_remainder=True).prefetch(1)

discriminator.compile(loss='binary_crossentropy', optimizer='rmsprop')
discriminator.training=False
gan.compile(loss='binary_crossentropy', optimizer='rmsprop')

train_gan(gan, dataset, batch_size, codings_size)

훈련 에포크 50번 후 DCGAN이 생성한 이미지(교재), 그냥 쌩 colab에서 train_gan() 함수를 실행하면 아마 하루보다 더 걸릴 것 같습니다.

 

이 구조를 조금 더 확장해서 대규모 얼굴 데이터셋에 훈련하면 꽤 실제 같은 이미지를 얻을 수 있습니다. 실제로 DCGAN은 상당히 의미 있는 잠재 표현을 학습할 수 있습니다.

 

 

생성한 많은 이미지들 중 안경을 쓴 남자 3명, 안경을 쓰지 않은 남자 3명, 안경을 쓰지 않은 여자 3명을 선택합니다. 그 후 3개 카테고리의 들 각각 이미지들의 코딩을 평균하고 이 평균 코딩으로 이미지를 새로 생성합니다. 이렇게 하면 각 카테고리에 있는 3개의 이미지들이 중첩된 이미지를 만드는데, 잠재 공간에서 계산된 평균이므로 이미지가 자연스러운 얼굴처럼 보입니다.

 

놀라운 점은 안경 쓴 남자의 평균 코딩 - 안경을 쓰지 않은 남자 평균 코딩 + 안경을 쓰지 않은 여자를 더한 코딩을 generator에 넣고 이미지를 생성하면 3x3 격자 가운데 있는 안경을 쓴 여자 이미지를 얻을 수 있습니다. 주위를 둘러싼 8개 이미지는 DCGAN의 시맨틱 보간 능력을 보여주기 위해 앞서 계산한 코딩에 약간의 잡음을 추가해 생성한 것입니다. 얼굴에 대해 수학 연산이 가능한 것입니다!

 

Tip

더보기

생성자와 판별자에 이미지 클래스를 추가적인 입력으로 넣는다면 클래스마다 어떻게 보이는지를 학습할 것입니다. 따라서 생성자가 만드는 이미지의 클래스를 조절할 수 있을 것입니다. 이를 조건 GAN(conditional GAN, CGAN)이라 부릅니다.

 

하지만 DCGAN은 완벽하지 않습니다. 예를 들어 DCGAN으로 매우 큰 이미지를 생성하면 국부적으로는 특징이 구분되지만 전반적으로는 일관성 없는 이미지를 얻을 가능성이 높습니다. 예를 들면 한쪽 소매가 더 긴 셔츠를 출력합니다. 이를 어떻게 하면 해결할 수 있을까요?

 

 

ProGAN

 

ProGAN은 훈련 초기에는 작은 이미지를 생성하고 점진적으로 생성자의 끝과 판별자의 시작 부분에 합성곱 층을 추가해 갈 수록 큰 이미지를 만드는 방법입니다. (4x4, 8x8, 16x16, ... ,512x512, 1024x1024)

 

방법은 적층 오토인코더를 층별로 훈련하는 것과 비슷합니다. 이전에 훈련된 층은 훈련 가능하게 두고 생성자의 끝과 판별자의 시작 부분에 층을 추가합니다.

 

 

  • 예를 들어 생성자의 출력은 4x4 → 8x8로 키우려면 기존 합성곱 층(합성곱 층 1)에서 (최근접 이웃 필터링을 사용한)  (UpSampling2D층의 interpolation='nearest'가 default임) 업샘플링 층을 추가해 8x8 크기의 특성 맵을 출력합니다.
  • 이 특성 맵이 새로운 합성곱 층(합성곱 층 2) → 새로운 출력 합성곱 층으로 주입됩니다. 기존 출력에 가중치 $1-α$, 새로운 출력에 가중치 $α$를 곱해서 더한 합을 최종 출력으로 만듭니다. $α$을 점진적으로 키워서 새로운 합성곱 층을 fade-in, 기존 합성곱 층을 fade-out합니다. 이렇게 하면 '합성곱 층1'의 훈련된 가중치를 잃지 않을 수 있습니다.
  • 판별자에도 새로운 합성곱 층을 추가할 때, 비슷하게 fade-in & fade-out 기법이 사용됩니다. (다운샘플링을 위해 평균 풀링 층이 뒤따릅니다?)
  • 모든 합성곱 층은 'same' 패딩과 stride=1을 사용해 공간 차원을 보존합니다. 
  • 출력 층의 커널 크기는 1이고, 판별자의 입력에 필요한 컬러 채널 수(보통 3)만큼 특성맵을 만들어 투영합니다.

합성곱 층은 커널을 슬라이딩해서 공간 차원을 결정하기 때문에 따로 기존 층에 스케일 작업을 하지 않아도 되는 것 같다.

 

 

ProGAN의 논문은 모드 붕괴를 막기 위해 출력의 다양성을 증가시키고 훈련을 더 안정적으로 만드는 다른 기법들도 소개합니다.

 

미니배치 표준 편차층

 

판별자 마지막 층 근처에 미니배치 표준편차 층을 추가합니다. 입력의 각 픽셀에 대해 배치의 모든 샘플과 샘플의 모든 채널에 걸쳐 표준 편차를 계산합니다. (S=tf.math.reduce_std(inputs, axis=[0, -1])) 이렇게 계산한 값을 모든 픽셀에 대해 평균하여 하나의 값을 얻습니다.(v=tf.reduce_mean(S)). 마지막으로 추가적인 특성 맵이 배치의 모든 샘플에 추가되고, 계산한 값 v를 채웁니다. (tf.concat([inputs, tf.fill([batch_size, height, width, 1], v), axis=-1))

 

이 값은 생성자가 만든 이미지에 다양성이 부족하면 특성맵 간의 표준편자가 작을 것이고, 다양성이 아주 적은 이미지를 만드는 생성자에게 속을 가능성이 줄어듭니다. 이는 생성자가 더 다양한 출력을 만들도록 유도해 모드 붕괴의 위험을 줄입니다.

 

동일한 학습속도

 

가중치를 초기화할 때 He 초기화 대신, 평균이 0, 표준편차가 1인 가우시안 분포를 사용해 모든 가중치를 초기화합니다. 대신 runtime(즉, 층이 실행될 때마다) He 초기화에 있는 동일한 인자 $\sqrt{2/n_{inputs}}$로 가중치를 나눕니다. $n_{inputs}$는 층의 입력 개수입니다. RMSProp, Adam이나 다른 적응형 그레디언트 옵티마이저를 사용했을 때 GAN 성능을 크게 향상한다는 것이 논문으로 입증됐습니다.

 

적응형 그레디언트 옵티마이저는 각자 추정한 표준편차로 그레디언트 업데이트를 정규화하는데, dynamic range(변수가 수용할 수 있는 가장 큰 값과 작은 값 사이의 비율)가 큰 파라미터는 훈련하는 데 오랜 시간이 걸리고, dynamic range가 작은 파라미터는 너무 빨리 업데이트 되어 불안정해질 수 있습니다. 가중치 초기화에서 스케일을 맞추지 않고 모델의 한 부분($n_{inputs}$)으로 스케일을 맞춤으로써 훈련 내내 모든 dynamic range를 동일하게 만듭니다. 따라서 모든 가중치가 동일한 속도로 학습되어서 훈련 속도와 안정성을 높입니다.

 

픽셀별 정규화 층

 

생성자의 합성곱 층 뒤에 모두 추가합니다. 이 층은 동일한 이미지의 동일한 위치(픽셀)에 있는 모든 활성화 값($a$?)을 채널에 대해 정규화합니다. ($\sqrt{E\left(a^2\right)}$으로 나눕니다. $E\left(X\right)는 평균$) 텐서플로 코드로 쓰면 inputs/tf.sqrt(tf.reduct_mean(tf.square(X), axis=-1, keepdims=True)+1e-8)입니다. 이 기법은 생성자와 판별자 사이의 과도한 경쟁으로 활성화 값이 폭주하는 것을 막습니다.

 

이런 기법을 모두 조합해 저자들은 극도로 고화질의 얼굴 이미지를 생성했습니다. (https://homl.info/progandemo, ㅋㅋ 오 개신기)

 

우리가 생각해 볼 것은 무엇을 '진짜 같다'라고 부르는 건지 입니다. 생성된 이미지의 다양성은 자동으로 평가할 수 있지만 품질을 판단하는 것은 훨씬 까다롭고 주관적인 문제입니다. 한 가지 방법은 사람이 직접 평가하는 것이지만 비용과 시간이 많이 듭니다. 논문 저자들은 생성된 이미지와 훈련 이미지의 국부적인 구조 사이의 유사도를 여러 규모로 측정하는 방법을 제안했습니다. 이 아이디어는 혁신적인 또 다른 구조인 StyleGAN을 탄생시켰습니다.

 

 

StyleGAN

 

엔비디아 팀은 2018년 논문에서 유명한 StyleGAN 구조를 소개했습니다. 저자들은 생성자에 스타일 트랜스퍼(style transfer) 기법을 사용해 생성된 이미지가 훈련 데이터의 다양한 크기의 국부적인 구조를 갖도록 만들었습니다. 이는 생성된 이미지의 품질을 크게 높여줍니다. 판별자와 손실 함수는 그대로 두고 생성자만 변경했습니다. 생성자는 네트워크 2개로 구성됩니다.

 

 

매핑 네트워크

 

8개의 MLP(다층 퍼셉트론)이 잠재 표현 $z$(즉, 코딩)을 벡터 $w$로 매핑합니다. 이 벡터 $w$는 아핀 변환(affine transformation, 그림에서 'A' 박스로 표시된 활성화 함수가 없는 Dense 층)으로 전달되어 벡터(그림에서 스타일) 여러 개를 생성합니다. 이렇게 생성된 벡터는 미세한 텍스처(ex, 머리 색상)부터 고수준 특성(ex, 어른이나 아이)까지 각기 다른 수준에서 생성된 이미지의 스타일을 제어합니다. 간단히 말해 매핑 네트워크는 코딩을 여러 스타일 벡터로 매핑합니다.

 

 

합성 네트워크

 

이미지 생성을 책임집니다. 이 네트워크는 일정하게 학습된 입력을 받습니다.? (정확하게는 훈련이 끝난 후에 입력이 일정해지고, 훈련 동안에는 역전파에 의해 계속 바뀝니다.) ProGAN에서와 같이 이 입력(그림에서 Const 4x4x512 인듯?)을 합성곱 여러 개와 업샘플링 층에 통과시킵니다. ProGAN과의 차이점이 2가지 있습니다.

 

https://velog.io/@ghgh5317/StyleGAN-v1-%EA%B0%84%EB%8B%A8%ED%95%98%EA%B2%8C-%EC%A0%95%EB%A6%AC

 

  • 첫째, 입력과 활성화 함수 전에 있는 모든 합성곱 층의 출력에 잡음이 조금 섞입니다.
  • 둘째, 잡음이 섞인 다음에 적응적 인스턴스 정규화(adaptive instance normalization, AdaIN) 층이 뒤따릅니다. 

AdaIN 층은 각 특성 맵을 독립적으로 표준화합니다. 즉, 특성 맵의 평균을 빼고 표준편차로 나눕니다. 표준화한 다음 스타일 벡터를 사용해 각 특성 맵의 스케일과 이동(offset)을 결정합니다. (스타일 벡터에는 특성 맵마다 하나의 스케일과 편향이 포함됩니다.) 스케일이 표준편차이고 이동이 평균인가?

 

주근 깨나 머리의 세세한 위치처럼 이미지의 어떤 부분은 매우 랜덤합니다. 이런 무작위성을 잡음을 통해 구현할 수 있습니다. 코딩에 독립적으로 잡음을 추가하는 것은 중요합니다. 초기 GAN은 이런 무작위성이 코딩이나 생성자 자체에서 만든 잡음에서 왔습니다. 코딩에서 잡음이 오면 코딩의 표현 능력의 상당 부분을 잡음을 저장하는 데 할애할 뿐 아니라 잡음이 네트워크에 흘러 생성자의 마지막 층까지 도달할 수 있어야 합니다. 이는 훈련 속도를 느리게 만들고 불필요한 제약 사항입니다. 또한, 각기 다른 수준(저~고수준)에서 동일한 잡음이 사용되면 이미지가 실제 같지 않고 인공적으로 보입니다. 이 또한 네트워크 능력을 낭비하는 것입니다. 

 

이러한 모든 이슈를 StyleGAN은 잡음 입력을 추가함으로써 이미지의 각 부분에 정확한 양의 무작위성을 추가할 수 있습니다. 

 

추가된 잡음은 각 수준(저~고수준)마다 다릅니다. 잡음 입력은 특성 맵 하나를 채우는 가우시안 잡음으로 구성됩니다. 이 특성맵은 해당 수준의 모든 특성 맵으로 브로드캐스팅됩니다. 그 다음 추가되기 전에 학습된 특성별 스케일링 인자로 조정됩니다.([그림 17-20]에서 'B' 박스)

 

마지막으로 StyleGAN은 한 이미지를 두 개의 다른 코딩을 사용해 생성하는 믹싱 규제(mixing regularization) 또는 스타일 믹싱(style mixing)이라 불리는 기법을 사용합니다. 구체적으로 코딩 $c_1$과 $c_2$가 매핑 네트워크를 통과해 스타일 벡터 $w_1$과 $w_2$를 만듭니다. 그 다음 합성 네트워크의 첫 번째 단계에서 스타일 $w_1$으로 나머지 단계에서는 $w_2$를 바탕으로 이미지를 생성합니다. 변경되는 단계?는 랜덤하게 선택됩니다. 이는 네트워크가 인접한 수준의 스타일이 상관관계를 가진다고 가정하지 못하도록 막습니다. 결국 각 스타일 벡터가 생성된 이미지에 있는 제한된 개수의 속성에만 영향을 미치는 styleGAN의 국지성을 촉진시킵니다.

 

GAN은 훈련하기 무척 어렵습니다. 하지만 GAN을 직접 구현해보면 정말 도움이 될 것입니다. 엔디비아의 공식 깃허브에 사전훈련된 StyleGAN 케라스 모델이 있습니다. (https://github.com/NVlabs/stylegan), (https://github.com/NVlabs/stylegan2)

 

 

출처: 핸즈온 머신러닝 2판

사진 출처: 핸즈온 머신러닝 2판