//
Search
📄

Variational Autoencoders (VAE)

VAE는 뉴럴넷으로 잠재변수 zz와 관측된 데이터 xx사이의 관계를 학습
잠재변수가 주어졌을 때 데이터에 대한 분포를 출력으로 가지는 네트워크를 decoder라 하며 pθ(xz)p_\theta(x|z)로 표현하며 θ\theta는 뉴럴넷의 가중치를 의미
θ\theta를 최적화 하여 p(xz)p(x|z)를 실제 분포에 가까워지도록 학습
zz의 사후분포(posterior)인 p(zx)p(z|x)를 뉴럴넷으로 엔코딩하며 이를 행하는 네트워크를 encoder라고 하고 qφ(zx)q_{\varphi}(z|x)로 표현
실제 사후분포에 가깝도록 encoder의 파라미터 φ\varphi를 최적화
본 챕터에선 VAE를 위한 두가지 효율적인 방법인 ELBOreparametrization을 소개

Evidence Lower Bound (ELBO)

encoder와 decoder의 파라미터를 최적화하기 위해 수식을 튜닝하는 방식으로 데이터분포 p(x)p(x)의 하한을 활용
임의의 관측데이터 xx의 원소를 x(i)x^{(i)}라 하고 데이터를 생성하는 잠재변수 zz가 존재한다고 가정
우린 잠재변수의 사후확률 p(zx(i))p(z|x^{(i)})가 존재한다는것은 알고 있으나 어떤분포인진 알 수 없으므로 이 분포가 어떤 잠재변수의 분포 qφ(zx(i))q_\varphi(z|x^{(i)})로 근사할 수 있다고 가정
이때 qq는 최적화를 쉽게 할 수 있어야하며 동시에 실제 사후확률을 정확하게 모델링 할 수 있을정도론 복잡도가 높아야 하므로 이를 뉴럴넷으로 설계
x(i)x^{(i)}의 확률인 p(x(i))p(x^{(i)})를 바로 사용하지 않고 최적화를 좀 더 편하게 하기 위해 logp(x(i))\log p(x^{(i)})를 최적화하면 아래와 같이 전개
p(x(i),z)=p(x(i))p(zx(i))p(x(i))=p(x(i),z)p(zx(i))logp(x(i))=logp(x(i),z)p(zx(i))=logp(x(i),z)logp(zx(i))=logp(x(i),z)logp(zx(i))+logqφ(zx(i))logqφ(zx(i))\begin{align*} &p(x^{(i)},z)=p(x^{(i)})\cdot p(z|x^{(i)}) \\ &\therefore p(x^{(i)})=\frac{p(x^{(i)},z)}{p(z|x^{(i)})} \\ & \log p(x^{(i)})=\log \frac{p(x^{(i)},z)}{p(z|x^{(i)})}=\log p(x^{(i)},z)-\log p(z|x^{(i)}) \\ &= \log p(x^{(i)},z)-\log p(z|x^{(i)}) +\log q_{\varphi}(z|x^{(i)}) - \log q_{\varphi}(z|x^{(i)}) \end{align*}
좌변인 logp(x(i))\log p(x^{(i)})은 상수이므로 양변에 Expectation 연산을 취해도 똑같으므로 양변에 Expectation 연산을 취하여 전개
logp(x(i))=Eqφ(zx(i))[logp(x(i),z)logp(zx(i))+logqφ(zx(i))logqφ(zx(i))]=Eqφ(zx(i))[logp(x(i),z)logqφ(zx(i))+logqφ(zx(i))logp(zx(i))]=Eqφ(zx(i))[logp(x(i),z)qφ(zx(i))]+Eqφ(zx(i))[logqφ(zx(i))p(zx(i))]=ELBO+KL(qφ(zx(i))p(zx(i)))\begin{align*} &\log p(x^{(i)}) \\ &=\mathbb{E}_{q_\varphi(z|x^{(i)})}[\log p(x^{(i)},z)-\log p(z|x^{(i)}) +\log q_{\varphi}(z|x^{(i)}) - \log q_{\varphi}(z|x^{(i)})] \\ &=\mathbb{E}_{q_\varphi(z|x^{(i)})}[\log p(x^{(i)},z)- \log q_{\varphi}(z|x^{(i)}) +\log q_{\varphi}(z|x^{(i)})-\log p(z|x^{(i)}) ] \\ &=\mathbb{E}_{q_\varphi(z|x^{(i)})}\bigg[\log{\frac{p(x^{(i)},z)}{q_{\varphi}(z|x^{(i)})}}\bigg] + \mathbb{E}_{q_\varphi(z|x^{(i)})}\bigg[\log{\frac{q_{\varphi}(z|x^{(i)})}{p(z|x^{(i)})}}\bigg] \\ &=\mathrm{ELBO} +\mathrm{KL}(q_{\varphi}(z|x^{(i)}) || p(z|x^{(i)})) \end{align*}
우리는 잠재변수의 분포 p(zx(i))p(z|x^{(i)})와 이를 근사하는 분포 qφ(zx(i))q_\varphi(z|x^{(i)})의 거리가 최소화되어야 하므로 KL(qφ(zx(i))p(zx(i)))\mathrm{KL}(q_{\varphi}(z|x^{(i)}) || p(z|x^{(i)}))를 최대화 시켜야함
이 때 logp(x(i))\log p(x^{(i)})는 상수이므로 KL\mathrm{KL}을 최대화 시키는것은 ELBO\mathrm{ELBO}를 최소화 시키는것과 같고 이를 이용하면 아래와 같이 ELBO\mathrm{ELBO}를 전개할 수 있음
앞에서 언급한것과 같이 p(zx(i))p(z|x^{(i)})는 모르므로 qφ(zx(i))q_\varphi(z|x^{(i)})로 근사하여 변형하면 아래와 같은 최종 최적화 수식을 얻을 수 있음
먼저 위 식을 최대화 하는 θ\theta^{*}를 구하기 위해 θ\theta에 대해 미분하면 아래와 같이 전개됨
φ\varphi^{*} 또한 같은 방법으로 아래와 같이 전개함
θ\theta^{*}를 구할 때와 달리 φ\varphi^{*}의 경우 위 형광색으로 표시한 φqφ(zx(i))logpθ(x(i)z)dz\int \triangledown_{\varphi}q_\varphi(z|x^{(i)})\log p_\theta (x^{(i)}|z)dz를 구하기가 까다로움
이를 위해 로그트릭(θ  p(τθ)=p(τθ)θlogp(τθ)\triangledown_\theta\;p(\tau|\theta)=p(\tau|\theta)\triangledown_\theta\log p(\tau|\theta))을 사용하여 φqφ(zx(i))=qφ(zx(i))φlogqφ(zx(i))\triangledown_{\varphi}q_\varphi(z|x^{(i)})=q_\varphi(z|x^{(i)})\triangledown_{\varphi}\log q_\varphi(z|x^{(i)})라 할 수있고 정리하면 φlogqφ(zx(i))=φqφ(zx(i))qφ(zx(i))\triangledown_{\varphi}\log q_\varphi(z|x^{(i)})=\frac{\triangledown_{\varphi}q_\varphi(z|x^{(i)})}{q_\varphi(z|x^{(i)})}가 됨
이를통해 φqφ(zx(i))\triangledown_{\varphi}q_\varphi(z|x^{(i)})를 아래와 같이 전개할 수 있음
위 식을 다시 위의 φ\triangledown_{\varphi}를 구하기 위한 식에 대입하면 아래와 같이 전개할 수 있음

Reparametrization trick

φ\varphi과 관련한 미분값이 낮은분산값을 가지도록 계산하기 위해 Reparametrization trick을 활용
zz가 encoder에 의해 생성되므로 다양한 zz값이 만들어지게 되며 이는 결국 encoder의 출력값이자 decoder의 입력값인 zz가 높은 분산값을 가지게 됨을 의미함
decoder에 들어갈 입력값이 배치마다 차이가 커지게 되므로 학습이 잘 안되며 이를 해결하기 위해 배치크기를 늘려 많은 샘플을 만들어 완화시킬 수 있으나 이런 방식은 계산량측면에서 비효율적임
즉, 분산이 작은 zz를 생성해야하며 이를 위해 encoder의 가중치와 상관없는 사후분포(주로 다중가우시안 분포 등) 근사를 활용
encoder의 출력인 qφ(zx(i))q_\varphi(z|x^{(i)})가 분포 N(z;μ^φ,σ^φ2I)N(z;\hat{\mu}_{\varphi}, \hat{\sigma}^2_\varphi I)를 따른다고 가정
다중 가우시안분포의 원소를 ziN(z;μ^φ,σ^φ2I)z_i\sim N(z;\hat{\mu}_{\varphi}, \hat{\sigma}^2_\varphi I)라 할 때 Reparametrization trick를 사용하면 아래와 같이 표현할 수 있음
zN(z;μ^φ,σ^φ2I)z=μ^φ+σ^φϵ,  ϵN(0,I)z\sim N(z;\hat{\mu}_{\varphi}, \hat{\sigma}^2_\varphi I)\Leftrightarrow z=\hat{\mu}_{\varphi}+ \hat{\sigma}_\varphi*\epsilon,\;\epsilon\sim N(0,I)
즉, zzgφ(ϵ)=μ^φ+σ^φϵg_\varphi(\epsilon)=\hat{\mu}_{\varphi}+ \hat{\sigma}_\varphi*\epsilon로 두고 아래와 같이 연산하며 ϵ\epsilon을 보조랜덤변수(auxiliary random variable)라 함
이를 통해 encoder의 출력에 바로 의존하지 않고 encoder의 분산과 평균에만 의존하여 출력이 낮은 분산값을 가지도록 유도
그러나 본 방식은 사후분포가 가우시안을 따른다는 보장이 없다는 단점을 가짐

전체 코드

# this code is made by chatgpt import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms from torch.utils.data import DataLoader import matplotlib.pyplot as plt # VAE 하이퍼파라미터 설정 batch_size = 128 learning_rate = 0.001 epochs = 20 latent_dim = 20 # 데이터 전처리 및 로드 transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) train_dataset = datasets.MNIST(root='./data', train=True, transform=transform, download=True) train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) # VAE 모델 정의 class VAE(nn.Module): def __init__(self, latent_dim): super(VAE, self).__init__() # Encoder self.encoder = nn.Sequential( nn.Linear(28*28, 400), nn.ReLU(), nn.Linear(400, 200), nn.ReLU(), ) self.fc_mu = nn.Linear(200, latent_dim) self.fc_logvar = nn.Linear(200, latent_dim) # Decoder self.decoder = nn.Sequential( nn.Linear(latent_dim, 200), nn.ReLU(), nn.Linear(200, 400), nn.ReLU(), nn.Linear(400, 28*28), nn.Sigmoid() ) def encode(self, x): h1 = self.encoder(x) mu = self.fc_mu(h1) logvar = self.fc_logvar(h1) return mu, logvar def reparameterize(self, mu, logvar): std = torch.exp(0.5 * logvar) eps = torch.randn_like(std) return mu + std * eps def decode(self, z): return self.decoder(z) def forward(self, x): mu, logvar = self.encode(x) z = self.reparameterize(mu, logvar) return self.decode(z), mu, logvar # 손실 함수 정의 def loss_function(recon_x, x, mu, logvar): # 재구성 손실 (Binary Cross Entropy) BCE = nn.functional.binary_cross_entropy(recon_x, x, reduction='sum') # KL 발산 (Kullback-Leibler Divergence) KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp()) return BCE + KLD # 모델 및 옵티마이저 초기화 model = VAE(latent_dim).cuda() optimizer = optim.Adam(model.parameters(), lr=learning_rate) # 학습 루프 for epoch in range(epochs): model.train() train_loss = 0 for batch_idx, (data, _) in enumerate(train_loader): data = data.cuda() data = data.view(-1, 28*28) optimizer.zero_grad() recon_batch, mu, logvar = model(data) loss = loss_function(recon_batch, data, mu, logvar) loss.backward() train_loss += loss.item() optimizer.step() if batch_idx % 100 == 0: print(f'Epoch [{epoch}/{epochs}] Batch [{batch_idx}/{len(train_loader)}] Loss: {loss.item() / len(data):.6f}') print(f'====> Epoch: {epoch} Average loss: {train_loss / len(train_loader.dataset):.6f}')
Python
복사

출처

Fundamentals of Deep Learning, 2nd Edition by Nithin Buduma, Nikhil Buduma, Joe Papa