๐ŸŽ“ A1Learner
๐Ÿ”

PyTorch Training a VAE

Training a VAE means training the encoder, the sampler, and the decoder together on the two-part loss. This lesson trains a VAE and a plain autoencoder on the same blobs and prints both reconstruction errors.

Every rebuild samples a fresh code

Two draws from the same distribution give two different codes. The seed changes between the draws here on purpose, so the difference shows.

dist = torch.distributions.Normal(torch.tensor([0.0]), torch.tensor([1.0]))
torch.manual_seed(1)
print(round(dist.rsample().item(), 4))
torch.manual_seed(2)
print(round(dist.rsample().item(), 4))

A far mean costs more KL

The KL term grows fast as the mean moves away from 0. A mean of 2 costs sixteen times what a mean of one half costs.

for m in [0.5, 2.0]:
    mean = torch.tensor([m])
    logvar = torch.tensor([0.0])
    kl = -0.5 * (1 + logvar - mean.pow(2) - logvar.exp())
    print(m, kl.item())

The two-part loss in the loop

The loop adds the reconstruction term and the KL term into one loss. One backward call then trains the whole model.

rebuilt, mean, logvar = vae(points)
recon = nn.functional.mse_loss(rebuilt, points)
kl = -0.5 * torch.mean(1 + logvar - mean.pow(2) - logvar.exp())
loss = recon + kl

Example

import torch
from torch import nn

torch.manual_seed(0)
per_blob = 50
blob_a = torch.randn(per_blob, 2) * 0.5 + torch.tensor([-2.0, -2.0])
blob_b = torch.randn(per_blob, 2) * 0.5 + torch.tensor([2.0, 2.0])
points = torch.cat([blob_a, blob_b])


class Autoencoder(nn.Module):
    def __init__(self):
        super().__init__()
        self.encoder = nn.Sequential(nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 1))
        self.decoder = nn.Sequential(nn.Linear(1, 16), nn.ReLU(), nn.Linear(16, 2))

    def forward(self, x):
        return self.decoder(self.encoder(x))


class VAE(nn.Module):
    def __init__(self):
        super().__init__()
        self.body = nn.Sequential(nn.Linear(2, 16), nn.ReLU())
        self.to_mean = nn.Linear(16, 1)
        self.to_logvar = nn.Linear(16, 1)
        self.decoder = nn.Sequential(nn.Linear(1, 16), nn.ReLU(), nn.Linear(16, 2))

    def encode(self, x):
        h = self.body(x)
        return self.to_mean(h), self.to_logvar(h)

    def forward(self, x):
        mean, logvar = self.encode(x)
        std = torch.exp(0.5 * logvar)
        z = torch.distributions.Normal(mean, std).rsample()
        return self.decoder(z), mean, logvar


torch.manual_seed(1)
ae = Autoencoder()
opt = torch.optim.Adam(ae.parameters(), lr=0.02)
for epoch in range(300):
    loss = nn.functional.mse_loss(ae(points), points)
    opt.zero_grad()
    loss.backward()
    opt.step()

torch.manual_seed(1)
vae = VAE()
opt = torch.optim.Adam(vae.parameters(), lr=0.02)
for epoch in range(300):
    rebuilt, mean, logvar = vae(points)
    recon = nn.functional.mse_loss(rebuilt, points)
    kl = -0.5 * torch.mean(1 + logvar - mean.pow(2) - logvar.exp())
    loss = recon + kl
    opt.zero_grad()
    loss.backward()
    opt.step()

with torch.no_grad():
    ae_error = nn.functional.mse_loss(ae(points), points).item()
    torch.manual_seed(5)
    rebuilt, mean, logvar = vae(points)
    vae_error = nn.functional.mse_loss(rebuilt, points).item()
    blob_a_mean = mean[:per_blob].mean().item()
    blob_b_mean = mean[per_blob:].mean().item()

print("Autoencoder reconstruction error:", round(ae_error, 4))
print("VAE reconstruction error:", round(vae_error, 4))
print("Blob A average latent mean:", round(blob_a_mean, 2))
print("Blob B average latent mean:", round(blob_b_mean, 2))

Key points

  • Both models here train on the same 100 points for 300 passes
  • The VAE pays for its ordered latent space with a higher reconstruction error
  • The two blobs take opposite ends of the latent line after training
  • A latent mean is the average code the encoder gives one blob

In this example, the plain autoencoder from the earlier lesson trains first on the 100 blob points. Its reconstruction error prints as 0.0876. The VAE then trains on the same points with the same seed and the same 300 passes. Its reconstruction error prints as 0.459, about five times higher. The VAE does not win on reconstruction, and the numbers say so plainly. Its gain shows in the latent means. Blob A averages -0.9 on the latent line, and blob B averages 0.88. The KL term forced the codes into one ordered line, and the blobs took opposite ends of it.

Note

The VAE error is measured on a seeded sample, so the number repeats on every run

Try it Yourself

Edit the code, then press Run.

Running is simulated ยท Python Playground

Exercise

A trained VAE rebuilds worse than a plain autoencoder on the same data. What does the VAE gain for that price?