논문 ‘Auto-Encoding Variational Bayes’ 리뷰
Variational Autoencoder(VAE)를 제안한 논문 ‘Auto-Encoding Variational Bayes’를 리뷰하겠습니다.
(논문 링크: https://arxiv.org/abs/1312.6114)
이 논문은 2013년 12월 네덜란드 암스테르담대(UvA)의 Diederik P Kingma, Max Welling이 발표했습니다. 2020년 현재 Max Welling 교수는 UvA에 여전히 계시고 Kingmas님은 구글 재직 중이신 듯 합니다. 논문은 현재까지 1만881 차례 인용됐으며, 오토인코더 관련 MUST-READ로 꼽히고 있습니다.
논문이 제시한 Auto-encoding variational bayes(AEVB) 알고리즘은 variational inference를 사용해 오토인코더를 최적화하는 방식입니다. 따라서 기본 개념인 오토인코더와 variational inference를 정리하는 순서로 AEVB 알고리즘을 요약하겠습니다. 이미지, 수식 등은 논문과 더불어 온라인 상 공유된 자료들을 참고했습니다. 참고자료 소스는 제일 하단에 기재돼 있습니다.
1. 오토인코더
오토인코더는 고차원의 입력 데이터(x)를 저차원의 잠재변수(latent variables; z)로 압축(=차원축소)한 뒤, 다시 입력 데이터에 가까운 고차원 데이터(x’)로 복원하는 구조의 모델입니다. 압축하는 부분을 ‘인코더(encoder)’, 복원하는 부분을 ‘디코더(decoder)’라고 부릅니다.
인코딩을 한다는 것은 데이터가 가진 수십, 수백개 변수로부터 정말 중요한 몇가지의 변수를 추출(extraction)한다는 뜻입니다. 일반 머신러닝에서 PCA 등으로 이를 수행한다면, 신경망 구조로 변수추출 내지는 차원축소를 수행하는 게 오토인코더입니다.
다른 차원축소 방법론과 오토인코더가 차별되는 것은 디코더 부분입니다. 인코더 뒤에 디코더를 붙이고, 출력해야 하는 정답 데이터로 입력 데이터를 사용함으로써 Self-supervised learning이 가능해졌습니다. 즉, 다른 정답 label 없이도 모델을 학습시킬 수 있기 때문에 어떤 데이터든 활용할 수 있게 된 것이죠.
이처럼 오토인코더의 주 목적은 차원축소였습니다. 하지만 이후 VAE가 나오면서 생성모델(generative model)로서의 용도가 더 각광받게 됐습니다. 저차원의 데이터를 고차원으로 ‘복원’해 새 데이터를 만들어낸다고 해서 디코더가 ‘generator’로 불리게 된 것도 이런 용도에서 입니다.
2. Variational inference
한국어로는 ‘변분추론’으로 번역되는 개념입니다. 딱히 이해를 돕는 한국어는 아닌 듯 해, Variational inference로 칭하겠습니다. 우선 수학적 유도를 빼고 직관적으로 설명하겠습니다.
Variational inference는, 어떤 사후확률 을 알고 싶을 때 이를 근사(approximate)한 확률분포를 상정한 뒤, 두 분포 간 차이를 수치화한 KL divergence를 최소화해 근사 분포를 구하는 접근법입니다. 이때 approximation분포는 다루기 쉬워야 하기 때문에 가우시안분포를 사용합니다. 이를 요약하면 아래와 같습니다.
이런 과정이 필요한 이유는 사후확률이 계산불가능(intractable)하기 때문입니다. 사후확률을 바로 계산하려면 p(x), p(z), p(x, z)를 알아야 하는데, 당연하게도 z에 대한 정보가 없기 때문입니다.
KL다이버전스를 최소화하기 위해 (figure 1.0)을 풀어쓰면 아래와 같이 다시 쓸 수 있습니다.
위 식에서 우변 첫번째 항이 (figure 1.0)에서 정의한 KL 다이버전스입니다. 좌변의 marginal likelihood는 항상 음수(p(x)가 0~1 사이의 값), KL다이버전스는 항상 양수의 값을 갖습니다. 때문에 우변 두번째항은 무조건 음수입니다. 이 항을 ‘Lower bound’(또는 ‘ELBO’)라고 부르는데, KL다이버전스가 가장 작은 값(0)일 때 marginal likelihood의 하한선이 된다는 뜻입니다.
(figure 1.1)에 따라 KL다이버전스를 최소화하는 문제는 Lower bound를 최대화하는 문제로 치환 가능합니다. 따라서 Lower bound를 다시 풀어쓰면 아래와 같이 정리됩니다.
이후 전개 과정은 논문이 제안한 방법론에 대한 설명이 필요하기 때문에 우선 이번 항목은 여기서 마치겠습니다. 여전히 우리가 풀고 싶은 문제는 최적의 approximation분포를 구하는 것이며, 이는 (figure 1.2)의 우변을 최대화하는 문제와 같습니다.
3. Variational 오토인코더(VAE)
VAE는 서두에 요약했듯이 2번의 Variational inference를 사용한 오토인코더입니다. 2번에서 근사하려는 확률분포가 인코더에 해당합니다. 입력 데이터 x를 z의 approximation 분포의 평균, 분산 벡터로 인코딩한 것이 오토인코더와 가장 큰 차이입니다. VAE는 이 확률분포의 파라미터(ϕ)를 조정해가며 잠재변수 z를 잘(=x와 가까운 x’를 만들어줄) 샘플링해 줄 최적의 확률분포를 찾는 문제로 요약할 수 있습니다.
(figure 1.2)에서 이어가겠습니다.
여기서 우변에 대한 중요한 해석이 가능합니다. 두번째 항은 주어진 q(z|x)에 대한 p(x|z)의 log값을 나타내기 때문에 Maximum Likelihood 문제가 됩니다. 그리고 첫번째 항인 KL다이버전스는 approximation분포를 z의 사전분포인 p(z)에 가깝게 만드는 regularizatioin 역할을 합니다. 따라서 이 문제는 제약이 있는 최적화 문제를 푸는 것으로 볼 수 있습니다.
이 식은 목적함수이자 손실함수입니다. 좌변에 마이너스를 곱하면 두번째 항을 ‘최소화’하는 문제가 되고, 이 항을 ‘Reconstruction error’ 또는 ‘Reconstruction loss’라고 부릅니다. 자기 자신을 얼마나 잘 복원시켰는지 측정하는 역할을 합니다. 아래는 손실함수에 대한 설명입니다.
- Reparameterization trick
(figure 1.2)에서 첫번째 KL다이버전스 항은 수학적으로 연산 가능하기 때문에, 문제는 두번째 항을 푸는 것만 남습니다. 하지만 z의 샘플링으로 인해 backpropagation 시 미분이 불가능하기 때문에 reparameterization trick을 사용합니다.
z를 함수 g로 표현함으로써 (평균, 분산)에 대해 미분이 가능하게 바꾸는 트릭입니다. 엡실론(노이즈)을 표준정규분포 N(0, I)로부터 샘플링한 후 평균과 표준편차를 이용하면 해당 평균과 표준편차에서 샘플링한 것과 같은 결과를 얻을 수 있습니다.
reparameterization을 적용한 z와, 두번째 항에 대해 Stochastic Gradient Variational Bayes(SGVB) estimator를 적용해 식을 다시 적으면 아래와 같이 정리됩니다.
마지막은 위 식을 평균, 분산 벡터에 대한 식으로 정리하는 것만 남았습니다. 아래 식을 이용해 코딩을 진행합니다.
4. Reproduction
그럼 VAE를 코드로 구현해 보겠습니다. 아래는 논문에 소개된 실험 중 MNIST 데이터를 모델링한 코드입니다. 아직 파이썬에 미숙한 점이 많아 코드는 github 공개자료를 참고하되, 각 코드에 대한 해설을 추가했습니다.
# 필요한 라이브러리 불러오기import torch
import torch.nn as nn
import numpy as np
from tqdm import tqdm
from torchvision.utils import save_image# 환경 및 하이퍼파라미터 설정dataset_path = '~/datasets'
DEVICE = torch.device("cpu")batch_size = 50
x_dim = 784 # 입력 데이터 차원(28*28)
hidden_dim = 400 # 히든 노드
latent_dim = 20 # 잠재변수 차원lr = 1e-3
epochs = 10# 데이터셋 불러오기from torchvision.datasets import MNIST
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
mnist_transform = transforms.Compose([
transforms.ToTensor(),
])
kwargs = {'num_workers': 1, 'pin_memory': True}
train_dataset = MNIST(dataset_path, transform=mnist_transform, train=True, download=True)
test_dataset = MNIST(dataset_path, transform=mnist_transform, train=False, download=True)
train_loader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True, **kwargs)
test_loader = DataLoader(dataset=test_dataset, batch_size=batch_size, shuffle=True, **kwargs)
### 모델 설계 #### 인코더 class Encoder(nn.Module):
def __init__(self, input_dim, hidden_dim, latent_dim):
super(Encoder, self).__init__()
self.FC_input = nn.Linear(input_dim, hidden_dim)
self.FC_mean = nn.Linear(hidden_dim, latent_dim)
self.FC_var = nn.Linear (hidden_dim, latent_dim)
self.training = True
def forward(self, x):
h_ = torch.relu(self.FC_input(x))
mean = self.FC_mean(h_)
log_var = self.FC_var(h_)
var = torch.exp(0.5*log_var)
z = self.reparameterization(mean, var)
return z, mean, log_var
# 표준정규분포에서 랜덤 추출한 엡실론, 인코더가 산출한 평균&분산으로 z 생성
def reparameterization(self, mean, var,):
epsilon = torch.rand_like(var).to(DEVICE)
z = mean + var*epsilonreturn z# 디코더 class Decoder(nn.Module):
def __init__(self, latent_dim, hidden_dim, output_dim):
super(Decoder, self).__init__()
self.FC_hidden = nn.Linear(latent_dim, hidden_dim)
self.FC_output = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
h = torch.relu(self.FC_hidden(x))
x_hat = torch.sigmoid(self.FC_output(h))
return x_hat# 모델 설계class Model(nn.Module):
def __init__(self, Encoder, Decoder):
super(Model, self).__init__()
self.Encoder = Encoder
self.Decoder = Decoder def forward(self, x):
z, mean, log_var = self.Encoder(x) # x -> z, mean, var
x_hat = self.Decoder(z) # z -> x_hat
return x_hat, mean, log_varencoder = Encoder(input_dim=x_dim, hidden_dim=hidden_dim, latent_dim=latent_dim) decoder = Decoder(latent_dim=latent_dim, hidden_dim = hidden_dim, output_dim = x_dim) model = Model(Encoder=encoder, Decoder=decoder).to(DEVICE)
### 손실함수(목적함수) 정의 ###from torch.optim import Adam
BCE_loss = nn.BCELoss()# reconstuction error와 KL다이버전스 값을 더한 loss함수 설정def loss_function(x, x_hat, mean, log_var):
reproduction_loss = nn.functional.binary_cross_entropy(x_hat, x, reduction='sum')
KLD = - 0.5 * torch.sum(1+ log_var - mean.pow(2) - log_var.exp())
return reproduction_loss + KLD
optimizer = Adam(model.parameters(), lr=lr)# 모델 학습(epoch=10)print("Start training VAE...")
model.train()
for epoch in range(epochs):
overall_loss = 0
for batch_idx, (x, _) in enumerate(train_loader):
x = x.view(batch_size, x_dim)
x = x.to(DEVICE)
optimizer.zero_grad()
x_hat, mean, log_var = model(x)
loss = loss_function(x, x_hat, mean, log_var)
overall_loss += loss.item()
loss.backward()
optimizer.step()
print("\tEpoch", epoch + 1, "complete!", "\tAverage Loss: ", overall_loss / (batch_idx*batch_size))
print("Finish!!")
# 모델 성능평가import matplotlib.pyplot as plt
model.eval()
with torch.no_grad():
for batch_idx, (x, _) in enumerate(tqdm(test_loader)):
x = x.view(batch_size, x_dim)
x = x.to(DEVICE)
x_hat, _, _ = model(x)# 생성 이미지 출력def show_image(x, idx):
x = x.view(batch_size, 28, 28)
fig = plt.figure()
plt.imshow(x[idx].cpu().numpy())show_image(x, idx=2)show_image(x_hat, idx=2)
마지막 출력 코드로 나온 이미지를 확인하면 다소 흐려지긴 했으나 원래의 이미지와 가깝게 복원된 것을 확인할 수 있습니다.
참고자료
- http://jaejunyoo.blogspot.com/2017/04/auto-encoding-variational-bayes-vae-1.html
- https://www.youtube.com/watch?v=KYA-GEhObIs
- https://blog.naver.com/dmsquf3015/221915171367
- 코드 참조1. https://github.com/Jackson-Kang/Pytorch-VAE-tutorial/blob/master/Variational_AutoEncoder.ipynb
- 코드 참조2. https://ratsgo.github.io/generative%20model/2018/01/27/VAE/