[핸즈온 머신러닝] 17장(3) - 변이형 오토인코더, Variational AutoEncoder
[AI/Hands-on ML] - [핸즈온 머신러닝] 17장(2) - 합성곱 오토인코더, 순환 오토인코더, 잡음 제거 오토인코더, 희소 오토인코더
[핸즈온 머신러닝] 17장(2) - 합성곱 오토인코더, 순환 오토인코더, 잡음 제거 오토인코더, 희소 오
[AI/Hands-on ML] - [핸즈온 머신러닝] 17장 - 오토인코더와 GAN을 사용한 표현 학습과 생성적 학습 [핸즈온 머신러닝] 17장 - 오토인코더와 GAN을 사용한 표현 학습과 생성적 학습 17. 오토인코더와 GAN 오
kdeon.tistory.com
17. 8 변이형 오토인코더, Variational AutoEncoder
2014년 소개된 오토인코더로, 매우 인기 있는 오토인코더 중 하나이다.
이전 글에서 봤던 오토인코더들은(Denoising AE, Sparse AE ...), input data가 encoder를 통해 데이터의 특징을 나타내는 하나의 encoding vector(=코딩)으로 변형되었다.
-> 즉, 이 encoder들은 하나의 single value를 출력했었고, decoder는 이 값을 사용해 원본 데이터를 복원하도록 학습이 되었다.
반면에 변이형 오토인코더(VAE)는, 잠재 공간(latent space)에서의 관찰을 설명하는 확률적 방식이다.
-> 다시 말해, VAE의 encoder는 각 특징을 설명하는 single value를 출력하는 것이 아닌, 각 latent attribute에 대한 확률 분포를 출력한다.
변이형 오토인코더의 특징
- 확률적 오토인코더 - 훈련이 끝난 후에도 출력이 부분적으로 우연에 의해 결정됨 (반면에 잡음 제거 오토인코더는 훈련 시에만 무작위성을 사용함)
- 생성 오토인코더 - 훈련 세트에서 샘플링된 것 같은 새로운 샘플을 생성
변이 인코더의 작동 방식
- 인코더 뒤에 디코더가 따르는 기본 구조
- 주어진 입력에 대한 코딩을 바로 만드는 대신, 인코더가 평균 코딩 μ와 표준편차 σ를 만듦
- 실제 코딩은 평균이 μ이고, 표준 편차가 σ인 가우시안 분포에서 랜덤하게 샘플링 됨
- 그 후 디코더가 샘플링된 코딩을 보통처럼 디코딩함
1. 인코더가 μ, σ를 만들면 코딩이 랜덤하게 선택됨
2. 코딩이 디코드되어 훈련 샘플을 닮은 최종 출력을 만듦
- 변이형 오토인코더는 입력이 복잡한 분포를 가지더라도, 간단한 가우시안 분포에서 샘플링된 것처럼 보이는 코딩을 만드는 경향이 있음
- 훈련하는 동안 비용 함수가 코딩을 가우시안 샘플들의 군집처럼 보이도록 코딩 공간(잠재 공간) 안으로 점진적으로 이동시킴
-> 변이형 오토인코더가 훈련이 끝난 뒤 새로운 샘플을 매우 쉽게 생성할 수 있음
=> 가우시안 분포에서 랜덤한 코딩을 샘플링해 디코딩하면 됨
비용함수
두 부분으로 구성
- 오토인코더가 입력을 재생산하도록 만드는 일반적인 재구성 손실
- 단순한 가우시안 분포에서 샘플된 것 같은 코딩을 가지도록 오토인코더를 강제하는 잠재 손실
- 목표 분포(가우시안 분포)와, 실제 코딩 분포 사이의 KL 발산을 사용
- 코딩 층으로 전달될 수 있는 정보 양을 제한하는(유용한 특성을 학습하도록) 가우시안 잡음 때문에 수식이 조금 복잡하지만, 아래 식으로 잠재 손실을 간단히 계산 가능
- L : 잠재 손실
- n : 코딩의 차원
- μi : i번째 코딩 원소의 평균
- σi : 코딩 원소의 표준편차
- 벡터 μ, σ는 인코더의 출력
변이형 오토인코더에서 인코더가 σ 가 아니라 γ = log(σ^2) 을 출력하도록 변경할 수 있음
- 수학적으로 안정적이고, 훈련 속도를 높임
패션 MNIST 데이터셋으로 변이형 오토인코더 구현
μ와 γ가 주어졌을 때 코딩을 샘플링하는 사용자 정의 층
from tensorflow import keras
K = keras.backend
class Sampling(keras.layers.Layer):
def call(self, inputs):
mean, log_var = inputs
K.random_normal(tf.shape(log_var)) * K.exp(log_var / 2) + mean
- 두 입력 mean (μ), log_var (γ, 즉 log(σ^2)) 을 받음
- random_normal() 함수를 통해 평균이 0이고, 표준편차가 1인 정규분포에서 (γ와 동일한 크기의) 랜덤한 벡터를 샘플링함
- 그 후 σ 를 만들기 위해 exp(γ / 2) 를 곱하고, 마지막으로 μ를 더한 결과를 반환
-> 평균이 μ이고 표준편차가 σ 인 정규분포에서 코딩 벡터를 샘플링한 것과 같은 결과
📝 keras.backend.random_normal()
- 생성할 tensor의 shape을 받아, 정규 분포에서 tensor를 반환함.
함수형 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.models.Model(
inputs=[inputs], outputs=[codings_mean, codings_log_var, codings])
- codings_mean (μ) 와 coding_log_var (γ) 를 출력하는 두 Dense 층이 동일한 입력 (두 번째 Dense 층의 출력)을 사용
- codings_mean (μ) 와 coding_log_var (γ) 를 Sampling 층으로 전달
- 마지막으로 variational_encoder 모델은 출력 세 개를 만듦
- 실제 사용하는 것은 outputs 중 마지막 출력인 codings
디코더 생성
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.models.Model(inputs=[decoder_inputs], outputs=[outputs])
- 함수형 API 대신 시퀀셜 API를 사용
- 지금까지 다룬 디코더와 사실상 동일하게 층을 단순히 쌓은 것
변이형 오토인코더 모델 생성
_, _, codings = variational_encoder(inputs)
reconstructions = variational_decoder(codings)
variational_ae = keras.models.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.)
variational_ae.compile(loss="binary_crossentropy", optimizer="rmsprop")
- 위에서 나왔던 수식을 통해 배치에 있는 각 샘플의 잠재 손실을 계산
- 그 다음, 배치에 있는 모든 샘플의 평균 손실을 계산하고 재구성 손실에 비례해 적절한 크기가 되도록 784로 나눔
- 변이형 오토인코더의 재구성 손실은 픽셀마다 재구성 오차의 합
- 하지만 케라스가 binary_crossentropy 손실을 계산할 때 합이 아닌 784개 전체 픽셀의 평균을 계산함
- 따라서 필요한 것보다 재구성 손실이 784배 적음
- 간단하게 하기위해 잠재 손실을 784로 나눔 => 최종 손실이 784배 작아지므로, 더 큰 학습률을 사용해야 한다는 것을 의미
- 문제 잘 맞는, RMSprop 옵티마이저 사용
오토인코더 훈련
history = variational_ae.fit(X_train, X_train, epochs=25, batch_size=128,
validation_data=(X_valid, X_valid))
17. 8. 1 패션 MNIST 이미지 생성하기
이미지 생성
codings = tf.random.normal(shape=[12, codings_size])
images = variational_decoder(codings).numpy()
- 가우시안 분포에서 랜덤한 코딩을 샘플링하여 디코딩
- 어느정도 그럴싸한 이미지가 생성됨
- 세부 튜닝 후 오래 훈련하면 더 좋아질 것
시맨틱 보간
- 변이형 오토인코더는 시맨틱 보간 수행 가능
- 픽셀 수준이 아닌, 코딩 수준에서의 이미지 보간
1. 두 이미지를 인코더에 통과시켜 얻은 두 코딩을 보간함
2. 그 후 보간된 코딩을 디코딩하여 최종 이미지를 얻음
-> 결과는 원본 이미지 사이에 있는 중간 이미지가 됨
codings_grid = tf.reshape(codings, [1, 3, 4, codings_size])
larger_grid = tf.image.resize(codings_grid, size=[5, 7])
interpolated_codings = tf.reshape(larger_grid, [-1, codings_size])
images = variational_decoder(interpolated_codings).numpy()
- 12개의 코딩을 생성하여, 이를 3x4 격자로 만듦
- tf.image.resize() 함수를 사용해 격자를 5x7 크기로 바꿈
- resize() 함수가 기본적으로 이중 선형 보간을 수행하므로, 늘어난 모든 행과 열이 보간된 코딩을 가짐
- 그 후 디코더로 이미지 생성
결과 이미지 (원본 - 테두리, 나머지 - 시맨틱 보간 결과)
지난 몇 년간 변이형 오토인코더가 널리 쓰였으나, GAN이 더 실제같은 이미지를 만들어 인기가 높아짐
참조