Deep learning / Autoencoders and generative
Variational Lossy Autoencoder in Python:
Contents
- Introduction to Variational Lossy Autoencoders
- VLAE Architecture
- Loss Function
- Training Process
- Latent Space Visualization
- Image Generation
- Image Reconstruction
- Lossy Compression
- Interpolation in Latent Space
- Conditional VLAE
- Disentangled Representations
- Real-life Example: Image Denoising
- Real-life Example: Anomaly Detection
- Limitations and Considerations
- Additional Resources
Introduction to Variational Lossy Autoencoders
Variational Lossy Autoencoders (VLAEs) are an extension of traditional autoencoders, designed to learn efficient data representations while allowing for controlled information loss. They combine the principles of variational autoencoders and lossy compression, making them particularly useful for tasks such as image compression and generation.
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
# Set up the device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
VLAE Architecture
The VLAE architecture consists of an encoder, a latent space, and a decoder. The encoder compresses the input data into a lower-dimensional latent representation, while the decoder reconstructs the data from this representation. The "lossy" aspect comes from the intentional discarding of some information during the encoding process.
class VLAE(nn.Module):
def __init__(self, input_dim, latent_dim):
super(VLAE, self).__init__()
self.encoder = nn.Sequential(
nn.Linear(input_dim, 256),
nn.ReLU(),
nn.Linear(256, 128),
nn.ReLU()
)
self.fc_mu = nn.Linear(128, latent_dim)
self.fc_logvar = nn.Linear(128, latent_dim)
self.decoder = nn.Sequential(
nn.Linear(latent_dim, 128),
nn.ReLU(),
nn.Linear(128, 256),
nn.ReLU(),
nn.Linear(256, input_dim),
nn.Sigmoid()
)
def encode(self, x):
h = self.encoder(x)
return self.fc_mu(h), self.fc_logvar(h)
def reparameterize(self, mu, logvar):
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
return mu + eps * std
def decode(self, z):
return self.decoder(z)
def forward(self, x):
mu, logvar = self.encode(x.view(-1, 784))
z = self.reparameterize(mu, logvar)
return self.decode(z), mu, logvar
Loss Function
The VLAE loss function combines reconstruction loss and KL divergence. The reconstruction loss measures how well the decoder can reconstruct the input from the latent representation. The KL divergence term regularizes the latent space, encouraging it to follow a standard normal distribution.
def loss_function(recon_x, x, mu, logvar):
BCE = nn.functional.binary_cross_entropy(recon_x, x.view(-1, 784), reduction='sum')
KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
return BCE + KLD
Training Process
Training a VLAE involves iterating through the dataset, passing the data through the model, computing the loss, and updating the model parameters using backpropagation. We use an optimizer like Adam to adjust the weights and biases of the neural network.
def train(model, dataloader, optimizer, epochs):
model.train()
for epoch in range(epochs):
train_loss = 0
for batch_idx, (data, _) in enumerate(dataloader):
data = data.to(device)
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()
print(f'Epoch {epoch+1}, Loss: {train_loss / len(dataloader.dataset):.4f}')
# Initialize model, optimizer, and dataloader
model = VLAE(784, 20).to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
train_loader = DataLoader(MNIST('.', train=True, download=True,
transform=transforms.ToTensor()),
batch_size=128, shuffle=True)
# Train the model
train(model, train_loader, optimizer, epochs=10)
Latent Space Visualization
One of the advantages of VLAEs is the ability to visualize and manipulate the latent space. We can project high-dimensional data into a 2D or 3D space for visualization, or generate new samples by sampling from the latent space.
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
def visualize_latent_space(model, dataloader):
model.eval()
latent_vectors = []
labels = []
with torch.no_grad():
for data, label in dataloader:
data = data.to(device)
mu, _ = model.encode(data.view(-1, 784))
latent_vectors.append(mu.cpu().numpy())
labels.append(label.numpy())
latent_vectors = np.concatenate(latent_vectors, axis=0)
labels = np.concatenate(labels, axis=0)
tsne = TSNE(n_components=2, random_state=42)
latent_tsne = tsne.fit_transform(latent_vectors)
plt.figure(figsize=(10, 8))
scatter = plt.scatter(latent_tsne[:, 0], latent_tsne[:, 1], c=labels, cmap='tab10')
plt.colorbar(scatter)
plt.title("t-SNE visualization of VLAE latent space")
plt.show()
# Visualize the latent space
visualize_latent_space(model, train_loader)
Image Generation
VLAEs can generate new images by sampling from the latent space and passing the samples through the decoder. This process allows us to create novel images that resemble the training data.
def generate_images(model, num_images=10):
model.eval()
with torch.no_grad():
z = torch.randn(num_images, 20).to(device)
generated = model.decode(z)
fig, axes = plt.subplots(1, num_images, figsize=(20, 2))
for i, ax in enumerate(axes):
ax.imshow(generated[i].view(28, 28).cpu().numpy(), cmap='gray')
ax.axis('off')
plt.show()
# Generate new images
generate_images(model)
Image Reconstruction
We can evaluate the quality of our VLAE by comparing original images with their reconstructed versions. This helps us understand how much information is preserved during the encoding-decoding process.
def reconstruct_images(model, dataloader):
model.eval()
dataiter = iter(dataloader)
images, _ = next(dataiter)
with torch.no_grad():
images = images.to(device)
reconstructed, _, _ = model(images)
fig, axes = plt.subplots(2, 10, figsize=(20, 4))
for i in range(10):
axes[0, i].imshow(images[i].cpu().view(28, 28), cmap='gray')
axes[0, i].axis('off')
axes[1, i].imshow(reconstructed[i].cpu().view(28, 28), cmap='gray')
axes[1, i].axis('off')
plt.show()
# Reconstruct images
reconstruct_images(model, train_loader)
Lossy Compression
The "lossy" aspect of VLAEs allows for controlled information loss, which can be useful in compression tasks. By adjusting the size and structure of the latent space, we can trade off between reconstruction quality and compression ratio.
def compress_image(model, image):
model.eval()
with torch.no_grad():
image = image.to(device)
mu, _ = model.encode(image.view(-1, 784))
return mu
def decompress_image(model, compressed):
model.eval()
with torch.no_grad():
decompressed = model.decode(compressed)
return decompressed
# Example usage
dataiter = iter(train_loader)
images, _ = next(dataiter)
original_image = images[0]
compressed = compress_image(model, original_image)
decompressed = decompress_image(model, compressed)
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
plt.imshow(original_image.cpu().view(28, 28), cmap='gray')
plt.title("Original")
plt.subplot(1, 2, 2)
plt.imshow(decompressed.cpu().view(28, 28), cmap='gray')
plt.title("Decompressed")
plt.show()
print(f"Compression ratio: {784 / compressed.numel():.2f}")
Interpolation in Latent Space
One interesting property of VLAEs is the ability to perform smooth interpolation between different images in the latent space. This can reveal interesting transitions and relationships between different data points.
def interpolate_images(model, img1, img2, steps=10):
model.eval()
with torch.no_grad():
z1, _ = model.encode(img1.view(-1, 784))
z2, _ = model.encode(img2.view(-1, 784))
alphas = torch.linspace(0, 1, steps)
interpolated_z = torch.stack([z1 * (1 - alpha) + z2 * alpha for alpha in alphas])
interpolated_images = model.decode(interpolated_z)
fig, axes = plt.subplots(1, steps, figsize=(20, 2))
for i, ax in enumerate(axes):
ax.imshow(interpolated_images[i].view(28, 28).cpu().numpy(), cmap='gray')
ax.axis('off')
plt.show()
# Example usage
dataiter = iter(train_loader)
images, _ = next(dataiter)
interpolate_images(model, images[0], images[1])
Conditional VLAE
We can extend the VLAE to incorporate conditional information, allowing us to generate or reconstruct images based on specific attributes or labels. This is useful for tasks like generating images of specific digit classes in MNIST.
class ConditionalVLAE(nn.Module):
def __init__(self, input_dim, latent_dim, num_classes):
super(ConditionalVLAE, self).__init__()
self.encoder = nn.Sequential(
nn.Linear(input_dim + num_classes, 256),
nn.ReLU(),
nn.Linear(256, 128),
nn.ReLU()
)
self.fc_mu = nn.Linear(128, latent_dim)
self.fc_logvar = nn.Linear(128, latent_dim)
self.decoder = nn.Sequential(
nn.Linear(latent_dim + num_classes, 128),
nn.ReLU(),
nn.Linear(128, 256),
nn.ReLU(),
nn.Linear(256, input_dim),
nn.Sigmoid()
)
self.num_classes = num_classes
def encode(self, x, c):
inputs = torch.cat([x, c], dim=1)
h = self.encoder(inputs)
return self.fc_mu(h), self.fc_logvar(h)
def decode(self, z, c):
inputs = torch.cat([z, c], dim=1)
return self.decoder(inputs)
def forward(self, x, c):
mu, logvar = self.encode(x.view(-1, 784), c)
z = self.reparameterize(mu, logvar)
return self.decode(z, c), mu, logvar
def reparameterize(self, mu, logvar):
std = torch.exp(0.5 * logvar)
eps = torch.randn_like(std)
return mu + eps * std
# Initialize and train the conditional VLAE (code omitted for brevity)
Disentangled Representations
VLAEs can be designed to learn disentangled representations, where different dimensions of the latent space correspond to independent factors of variation in the data. This can be achieved by modifying the loss function to encourage independence between latent dimensions.
def disentangled_loss(recon_x, x, mu, logvar, beta=4.0):
BCE = nn.functional.binary_cross_entropy(recon_x, x.view(-1, 784), reduction='sum')
KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
return BCE + beta * KLD
class DisentangledVLAE(VLAE):
def __init__(self, input_dim, latent_dim):
super(DisentangledVLAE, self).__init__(input_dim, latent_dim)
def forward(self, x):
mu, logvar = self.encode(x.view(-1, 784))
z = self.reparameterize(mu, logvar)
return self.decode(z), mu, logvar
# Train the disentangled VLAE using the modified loss function
model = DisentangledVLAE(784, 10).to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(50):
for batch_idx, (data, _) in enumerate(train_loader):
data = data.to(device)
optimizer.zero_grad()
recon_batch, mu, logvar = model(data)
loss = disentangled_loss(recon_batch, data, mu, logvar)
loss.backward()
optimizer.step()
print(f'Epoch {epoch+1}, Loss: {loss.item() / len(data):.4f}')
Real-life Example: Image Denoising
VLAEs can be used for image denoising by training the model on pairs of noisy and clean images. The model learns to map noisy inputs to their clean counterparts in the latent space.
import numpy as np
def add_noise(image, noise_factor=0.5):
noisy_image = image + noise_factor * torch.randn(*image.shape)
return torch.clamp(noisy_image, 0., 1.)
class DenoisingVLAE(VLAE):
def __init__(self, input_dim, latent_dim):
super(DenoisingVLAE, self).__init__(input_dim, latent_dim)
def forward(self, x_noisy, x_clean):
mu, logvar = self.encode(x_noisy.view(-1, 784))
z = self.reparameterize(mu, logvar)
x_recon = self.decode(z)
return x_recon, mu, logvar
# Train the denoising VLAE
model = DenoisingVLAE(784, 20).to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(50):
for batch_idx, (data, _) in enumerate(train_loader):
clean_data = data.to(device)
noisy_data = add_noise(clean_data).to(device)
optimizer.zero_grad()
recon_batch, mu, logvar = model(noisy_data, clean_data)
loss = loss_function(recon_batch, clean_data, mu, logvar)
loss.backward()
optimizer.step()
print(f'Epoch {epoch+1}, Loss: {loss.item() / len(data):.4f}')
# Denoise an image
def denoise_image(model, noisy_image):
model.eval()
with torch.no_grad():
denoised, _, _ = model(noisy_image, None)
return denoised.view(28, 28)
# Example usage
test_image = next(iter(test_loader))[0][0].to(device)
noisy_image = add_noise(test_image)
denoised_image = denoise_image(model, noisy_image)
plt.figure(figsize=(15, 5))
plt.subplot(1, 3, 1)
plt.imshow(test_image.cpu().numpy().squeeze(), cmap='gray')
plt.title("Original Image")
plt.subplot(1, 3, 2)
plt.imshow(noisy_image.cpu().numpy().squeeze(), cmap='gray')
plt.title("Noisy Image")
plt.subplot(1, 3, 3)
plt.imshow(denoised_image.cpu().numpy().squeeze(), cmap='gray')
plt.title("Denoised Image")
plt.show()
Real-life Example: Anomaly Detection
VLAEs can be employed for anomaly detection in various domains, such as manufacturing or healthcare. By training the model on normal data, it learns to reconstruct normal patterns accurately. Anomalies can then be detected by measuring the reconstruction error.
def anomaly_score(model, x):
model.eval()
with torch.no_grad():
x_recon, mu, logvar = model(x)
recon_error = nn.functional.mse_loss(x_recon, x.view(-1, 784), reduction='none').sum(dim=1)
kl_div = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1)
return recon_error + kl_div
# Train VLAE on normal data (assuming '0' is the normal class)
normal_data = [(img, label) for img, label in train_loader.dataset if label == 0]
normal_loader = DataLoader(normal_data, batch_size=128, shuffle=True)
model = VLAE(784, 20).to(device)
optimizer = optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(50):
for batch_idx, (data, _) in enumerate(normal_loader):
data = data.to(device)
optimizer.zero_grad()
recon_batch, mu, logvar = model(data)
loss = loss_function(recon_batch, data, mu, logvar)
loss.backward()
optimizer.step()
# Detect anomalies
def detect_anomalies(model, dataloader, threshold):
anomalies = []
for data, labels in dataloader:
data = data.to(device)
scores = anomaly_score(model, data)
anomalies.extend([(img, label, score.item()) for img, label, score in zip(data, labels, scores) if score > threshold])
return anomalies
# Example usage
threshold = 100 # This should be tuned based on the specific use case
anomalies = detect_anomalies(model, test_loader, threshold)
print(f"Detected {len(anomalies)} anomalies")
for img, label, score in anomalies[:5]:
plt.figure(figsize=(3, 3))
plt.imshow(img.cpu().numpy().squeeze(), cmap='gray')
plt.title(f"Label: {label}, Score: {score:.2f}")
plt.show()
Limitations and Considerations
While VLAEs are powerful tools for various tasks, they have some limitations to consider:
- Training complexity: VLAEs can be challenging to train, especially for high-dimensional data or complex distributions.
- Hyperparameter sensitivity: The performance of VLAEs can be sensitive to hyperparameters like the latent space dimension and the balance between reconstruction and KL divergence losses.
- Interpretability: Although VLAEs can learn meaningful latent representations, interpreting these representations can still be challenging, especially for complex data.
- Scalability: For very large datasets or high-resolution images, training VLAEs can be computationally expensive and time-consuming.
- Mode collapse: In some cases, VLAEs might suffer from mode collapse, where they fail to capture the full diversity of the input distribution.
# Pseudocode for addressing some limitations
# 1. Use learning rate scheduling to help with training stability
scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min')
# 2. Implement beta-VAE for better disentanglement
def beta_vae_loss(recon_x, x, mu, logvar, beta):
BCE = F.binary_cross_entropy(recon_x, x.view(-1, 784), reduction='sum')
KLD = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
return BCE + beta * KLD
# 3. Visualize latent space traversals for interpretability
def latent_traversal(model, data, dim, range_):
# Implementation details omitted for brevity
# 4. Use data parallelism for better scalability
model = nn.DataParallel(model)
# 5. Implement techniques like batch normalization or weight normalization
class ImprovedVLAE(nn.Module):
def __init__(self, input_dim, latent_dim):
super(ImprovedVLAE, self).__init__()
# Add batch normalization layers
# Implementation details omitted for brevity
Additional Resources
For those interested in diving deeper into Variational Lossy Autoencoders and related topics, here are some valuable resources:
- "Auto-Encoding Variational Bayes" by Kingma and Welling (2013) ArXiv: https://arxiv.org/abs/1312.6114
- "β-VAE: Learning Basic Visual Concepts with a Constrained Variational Framework" by Higgins et al. (2017) ICLR 2017
- "Understanding disentangling in β-VAE" by Burgess et al. (2018) ArXiv: https://arxiv.org/abs/1804.03599
- "Variational Lossy Autoencoder" by Chen et al. (2016) ArXiv: https://arxiv.org/abs/1611.02731
- "An Introduction to Variational Autoencoders" by Kingma and Welling (2019) ArXiv: https://arxiv.org/abs/1906.02691
These papers provide in-depth explanations of the theoretical foundations and practical applications of VLAEs and related models.