Variational Inference: Bayesian Approximation

Inférence Variationnelle : Guide Complet — Approximation Bayésienne

Variational Inference: The Complete Guide to Bayesian Approximation

Summary

Variational inference is a powerful Bayesian approximation method that transforms a statistical inference problem into an optimization problem. Rather than directly computing posterior distributions that are often intractable, we seek the best possible approximation within a tractable family of distributions. This approach has become a pillar of modern machine learning, with applications ranging from variational autoencoders to Bayesian large language models.

In this guide, we will explore the mathematical foundations of variational inference in depth, its intuition, and we will write working Python code with PyTorch and Pyro to put it into practice.

Mathematical Principle

The Fundamental Problem

In Bayesian inference, we want to compute the posterior distribution p(z|x) of latent variables z given observations x. According to Bayes’ theorem:

p(z|x) = p(x|z) · p(z) / p(x)

The denominator p(x) — called the evidence or marginal likelihood — requires an integral over the entire latent space:

p(x) = ∫ p(x|z) · p(z) dz

In virtually all practical cases, this integral is intractable. It cannot be computed analytically, and direct numerical sampling is prohibitively expensive in high-dimensional spaces. This is precisely where variational inference comes in.

The Goal: Approximate the Posterior with a Tractable Distribution

The core idea of variational inference is to replace the exact computation of the posterior with an optimized approximation. Instead of seeking p(z|x) directly, we introduce a variational distribution q(z;ϕ), parameterized by ϕ, which belongs to a family of distributions we know how to manipulate analytically (typically factorized Gaussians).

We then seek the distribution q(z;ϕ) that is as close as possible to the true posterior p(z|x), according to a probabilistic distance measure. The standard measure is the Kullback-Leibler divergence (KL divergence):

KL(q(z;ϕ) || p(z|x)) = E_q[log q(z;ϕ) - log p(z|x)]

Minimizing this divergence makes q(z;ϕ) as similar as possible to p(z|x) in the KL sense.

Maximizing the ELBO

The direct problem is that KL(q || p) itself depends on p(x), which is intractable. The fundamental mathematical trick is to reformulate the log-evidence:

log p(x) = ELBO(ϕ) + KL(q(z;ϕ) || p(z|x))

Since log p(x) does not depend on ϕ and KL ≥ 0, we obtain a lower bound:

log p(x) ≥ ELBO(ϕ)

The ELBO (Evidence Lower Bound) is therefore a lower bound on the log-evidence. Maximizing the ELBO is exactly equivalent to minimizing the KL divergence, which is our original goal, but without ever having to compute p(x).

The ELBO is written in its most common and useful form as:

ELBO(ϕ) = E_q[log p(x|z)] - KL(q(z;ϕ) || p(z))

Each of the two terms has a remarkably clear and intuitive interpretation:

  • E_q[log p(x|z)]: the reconstruction term. It measures how well the distribution q(z;ϕ) explains the observed data x through the generative model p(x|z). It is a data fidelity term: if q concentrates its mass on values of z that generate the observations well through the model, this term will be high. The more faithful the reconstructions are to the real data, the better this term.
  • KL(q(z;ϕ) || p(z)): the regularization term. It measures how much the variational distribution q(z;ϕ) deviates from the prior p(z). This term prevents q from diverging arbitrarily; it keeps the approximation consistent with our prior beliefs before seeing the data. It is a complexity penalty that favors simple solutions.

This is a classic trade-off in machine learning: fidelity to observed data against conformity to prior assumptions. The ELBO achieves exactly this trade-off in a mathematically elegant and algorithmically exploitable way.

The Mean-Field Approximation: Factorizing Latent Variables

A very common simplifying assumption in practice is the mean-field approximation. It consists of assuming that all latent variables are mutually independent under the variational distribution:

q(z;ϕ) = ∏ᵢ qᵢ(zᵢ;ϕᵢ)

For example, if we have two latent variables z = (z₁, z₂), the mean-field approximation assumes q(z) = q₁(z₁) × q₂(z₂). Each component can then be optimized separately, which greatly simplifies analytical and numerical calculations.

The major advantage is computational tractability: we reduce a complex multidimensional problem to a collection of simple one-dimensional problems. The important drawback is that this assumption ignores any correlation between latent variables, which can significantly degrade the quality of the approximation when variables are strongly dependent in the true posterior.

The Reparameterization Trick: Making Sampling Differentiable

A major obstacle to optimizing the ELBO by gradient descent is that the expectation E_q involves sampling from q(z;ϕ), a fundamentally non-differentiable operation. We cannot perform backpropagation through a direct sample from a probability distribution.

The revolutionary solution is the reparameterization trick (also called the Kingma and Welling trick). Instead of sampling z directly from q(z;ϕ), we re-express the sampling as a deterministic and differentiable transformation of an auxiliary random variable ε whose distribution is fixed and independent of the parameters:

z = μ + σ · ε    where    ε ~ N(0, 1)

Here, μ and σ are the parameters of q (for example a Gaussian N(μ, σ²)). The randomness comes only from ε, which is sampled independently of the parameters μ and σ. The transformation μ + σ · ε is entirely differentiable with respect to μ and σ, which allows backpropagation through the stochastic sampling operation.

This makes it possible to compute the gradient of the ELBO by standard backpropagation, exactly as in a classical neural network. This technical innovation has made variational inference deeply compatible with deep learning and massive neural architectures.

Intuition: The Suspect’s Composite Sketch

Variational inference is like drawing a suspect’s portrait from vague and contradictory witness accounts.

Imagine you are a forensic artist at a police station. A crime took place at night and several witnesses describe the suspect to you, but in an imprecise, incomplete, and sometimes contradictory way: “he had dark hair,” “rather tall perhaps,” “a face marked by a scar.” The true face of the suspect — the posterior p(z|x) — is of such complexity that it is impossible to reproduce it exactly from these fragmentary descriptions. No witness has a perfect view of the scene.

So, you work differently. You have a repertoire of artistic techniques that you have mastered: pencil drawing, charcoal, watercolor, dry pastel. Each technique corresponds to a variational family — that is, the set of portraits you truly know how to draw with skill. You decide to use pencil because it is the most versatile and proven technique for this particular type of description.

Now you truly begin to draw. The first sketch is necessarily rough and hesitant: an approximate oval for the face, very basic features for the eyes and nose. This is your initialization ϕ₀, the starting point of your optimization. Then you progressively refine: the witnesses repeated “rather tall,” you stretch the face upward. The hair is “dark,” you darken the upper area of the skull. Each successive adjustment corresponds to a parameter update ϕ that brings your drawing closer to the unknown reality.

Throughout this iterative and patient process, two opposing forces act on your artistic work:

  1. Fidelity to the witness accounts received (reconstruction term): you continuously adjust the features to match the descriptions provided by the witnesses as closely as possible. The more your portrait “explains” the witness accounts coherently, the better this term of the ELBO.
  2. Respect for your personal technical style (regularization term): you cannot draw absolutely anything. Your hand has its ingrained habits, its natural and physiological limits. You will never draw a perfect photorealistic portrait with a simple pencil. This inherent stylistic constraint is your prior — the fundamental idea that the portrait must remain within the domain of what the chosen technique allows and permits.

The final result is obviously not an exact photograph of the real suspect, but it is the best possible and plausible portrait within your repertoire of artistic skills. This is exactly and precisely what variational inference does: it finds the best mathematical compromise between what the data tells us and what our family of distributions can represent.

And just as an experienced artist quickly recognizes their own stylistic biases (“I systematically tend to draw noses too long”), variational inference honestly reveals its own intrinsic approximations. The mean-field approximation, for example, is like an artist who deliberately ignores the relationships and correlations between the different facial features — one eye drawn independently and separately from the other, without considering their symmetry. The final result is recognizable and useful, but it necessarily lacks certain important subtleties and nuances.

Python Implementation

1. Variational Inference from Scratch with PyTorch

Let’s start with a complete implementation of variational inference using only PyTorch, with the reparameterization trick.

import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt

# ─── Synthetic data generation ───
np.random.seed(42)
N = 200
X = np.linspace(-3, 3, N).reshape(-1, 1)
y = 2.0 * X + 1.0 + np.random.randn(N, 1) * 0.5

X_tensor = torch.tensor(X, dtype=torch.float32)
y_tensor = torch.tensor(y, dtype=torch.float32)


# ─── Variational network (guide) ───
class VariationalGuide(nn.Module):
    """
    Variational distribution q(w; phi) = N(mu_phi, diag(sigma_phi)^2)

    This network produces the parameters (mu, log_sigma) of the
    variational distribution over the weights w of the regression model.
    """

    def __init__(self, input_dim, latent_dim=2):
        super().__init__()
        self.latent_dim = latent_dim
        self.mu_net = nn.Sequential(
            nn.Linear(input_dim, 32),
            nn.ReLU(),
            nn.Linear(32, latent_dim)
        )
        self.log_sigma_net = nn.Sequential(
            nn.Linear(input_dim, 32),
            nn.ReLU(),
            nn.Linear(32, latent_dim)
        )

    def forward(self, x):
        """Returns (mu, log_sigma) for the distribution q(z|x)."""
        mu = self.mu_net(x)
        log_sigma = self.log_sigma_net(x)
        return mu, log_sigma

    def sample(self, x, num_samples=1):
        """
        Samples z from q(z|x) via the reparameterization trick.

        z = mu + sigma * epsilon  where  epsilon ~ N(0, 1)

        This formulation makes sampling differentiable with respect
        to the parameters mu and sigma, enabling backpropagation.
        """
        mu, log_sigma = self.forward(x)
        sigma = torch.exp(log_sigma)
        epsilon = torch.randn(num_samples, x.size(0), self.latent_dim)
        z = mu.unsqueeze(0) + sigma.unsqueeze(0) * epsilon
        return z


# ─── Bayesian model (likelihood and prior) ───
class BayesianLinearModel(nn.Module):
    """
    Bayesian linear regression model.

    Prior: p(w) = N(0, I)
    Likelihood: p(y|x, w) = N(x^T w, sigma_obs^2)
    """

    def __init__(self):
        super().__init__()
        self.sigma_obs = 0.5  # fixed observation noise

    def log_likelihood(self, x, y, w):
        """Computes log p(y|x, w) for each Monte Carlo sample."""
        predictions = w[:, :, 0] * x.squeeze(-1).unsqueeze(0) + w[:, :, 1]
        residuals = y.squeeze(-1).unsqueeze(0) - predictions
        ll = -0.5 * (residuals / self.sigma_obs) ** 2
        ll -= 0.5 * torch.log(2 * torch.tensor(np.pi) * self.sigma_obs ** 2)
        return ll.sum(dim=-1)

    def log_prior(self, w):
        """Computes log p(w) = log N(w; 0, I)."""
        return -0.5 * torch.sum(w ** 2, dim=-1)


# ─── Monte Carlo ELBO computation ───
def compute_elbo(guide, model, x, y, num_samples=10):
    """
    Computes the Monte Carlo ELBO with the reparameterization trick.

    ELBO ≈ (1/S) Σ_s [ log p(y|x, z_s) + log p(z_s) - log q(z_s|x) ]
    """
    z_samples = guide.sample(x, num_samples)
    mu, log_sigma = guide.forward(x)

    # log p(y|x, w) — reconstruction term
    ll = model.log_likelihood(x, y, z_samples)

    # log q(z|x) — log density of the variational Gaussian distribution
    log_q = -0.5 * log_sigma.shape[-1] * np.log(2 * np.pi) \
            - log_sigma.sum(dim=-1) \
            - 0.5 * ((z_samples - mu.unsqueeze(0))
                     / torch.exp(log_sigma.unsqueeze(0))) ** 2
    log_q = log_q.sum(dim=-1)

    # log p(w) — log of the standard Gaussian prior
    log_prior = model.log_prior(z_samples)

    # ELBO averaged over Monte Carlo samples
    elbo_per_sample = ll + log_prior - log_q
    return elbo_per_sample.mean()


# ─── Training loop ───
def train_vi(guide, model, X_tensor, y_tensor,
             num_steps=5000, lr=0.01, num_samples=10):
    optimizer = optim.Adam(guide.parameters(), lr=lr)
    losses = []
    for step in range(num_steps):
        optimizer.zero_grad()
        elbo = compute_elbo(guide, model, X_tensor, y_tensor, num_samples)
        loss = -elbo  # we minimize the loss = -ELBO
        loss.backward()
        optimizer.step()
        losses.append(loss.item())
        if (step + 1) % 500 == 0:
            print(f"Step {step + 1}/{num_steps} "
                  f"| ELBO: {elbo.item():.2f} "
                  f"| Loss: {loss.item():.2f}")
    return losses


if __name__ == "__main__":
    guide = VariationalGuide(input_dim=1, latent_dim=2)
    model = BayesianLinearModel()

    print("=" * 60)
    print("Variational Inference — Bayesian Linear Regression")
    print("=" * 60)

    losses = train_vi(guide, model, X_tensor, y_tensor,
                      num_steps=3000, lr=0.005, num_samples=20)

    # Results visualization
    fig, axes = plt.subplots(1, 2, figsize=(14, 5))

    axes[0].plot(losses, 'b-', linewidth=1.5)
    axes[0].set_title("ELBO Convergence Curve")
    axes[0].set_xlabel("Optimization Steps")
    axes[0].set_ylabel("Loss (-ELBO)")
    axes[0].grid(True, alpha=0.3)

    # Predictions with epistemic uncertainty
    x_vals = torch.linspace(-3, 3, 100).reshape(-1, 1)
    w_samples = guide.sample(torch.ones_like(x_vals), num_samples=50)
    pred_samples = (w_samples[:, :, 0] * x_vals.squeeze(-1).unsqueeze(0)
                    + w_samples[:, :, 1])

    pred_mean = pred_samples.mean(dim=0).numpy()
    pred_std = pred_samples.std(dim=0).numpy()

    axes[1].scatter(X, y, alpha=0.4, s=15, label="Observed data")
    axes[1].plot(x_vals.numpy(), pred_mean, 'r-', linewidth=2,
                 label="VI Prediction (mean)")
    axes[1].fill_between(
        x_vals.numpy().squeeze(),
        pred_mean - 2 * pred_std,
        pred_mean + 2 * pred_std,
        alpha=0.2, color='red',
        label="95% Confidence Interval"
    )
    axes[1].legend()
    axes[1].set_title("Bayesian Regression with Uncertainty")
    axes[1].set_xlabel("x")
    axes[1].set_ylabel("y")

    plt.tight_layout()
    plt.savefig("vi_regression_result.png", dpi=150)
    print("\nPlot saved: vi_regression_result.png")
    print(f"Learned parameters: w ≈ {guide.mu_net(torch.zeros(1, 1)).detach().numpy().flatten()[0]:.4f}")
    print(f"Uncertainty (log_sigma) ≈ {guide.log_sigma_net(torch.zeros(1, 1)).detach().numpy().flatten()[0]:.4f}")

2. Bayesian Model with Pyro: Probabilistic Programming

Pyro is the probabilistic programming library built on PyTorch, originally developed by Uber AI. It offers an elegant interface for specifying Bayesian models and their associated variational guide.

import pyro
import pyro.distributions as dist
from pyro.infer import SVI, Trace_ELBO
from pyro.optim import Adam
import torch

pyro.clear_param_store()
pyro.set_rng_seed(42)


# ─── Bayesian generative model ───
def model(x, y=None):
    """
    Bayesian linear regression model in Pyro.

    Prior on weights: w ~ N(0, 10·I) — broad, non-informative prior
    Prior on bias:     b ~ N(0, 10)
    Likelihood:        y ~ N(x·w + b, σ²)
    """
    w = pyro.sample(
        "w",
        dist.Normal(0.0, 10.0).expand([x.shape[1]]).to_event(1)
    )
    b = pyro.sample("b", dist.Normal(0.0, 10.0))

    mean = x @ w + b

    if y is not None:
        with pyro.plate("data", x.shape[0]):
            pyro.sample("obs", dist.Normal(mean, 1.0), obs=y)

    return mean


# ─── Variational guide ───
def guide(x, y=None):
    """
    Variational guide for Bayesian linear regression.

    q(w) = N(µ_w, σ_w²) — parameters learned by variational optimization
    q(b) = N(µ_b, σ_b²) — parameters learned by variational optimization
    """
    w_loc = pyro.param("guide_w_loc", torch.zeros(x.shape[1]))
    w_scale = pyro.param(
        "guide_w_scale",
        torch.ones(x.shape[1]),
        constraint=dist.constraints.positive
    )
    pyro.sample("w", dist.Normal(w_loc, w_scale).to_event(1))

    b_loc = pyro.param("guide_b_loc", torch.tensor(0.0))
    b_scale = pyro.param(
        "guide_b_scale",
        torch.tensor(1.0),
        constraint=dist.constraints.positive
    )
    pyro.sample("b", dist.Normal(b_loc, b_scale))


# ─── ELBO Training ───
def train_pyro_model(x_tensor, y_tensor, num_iterations=2000, lr=0.01):
    pyro.clear_param_store()

    svi = SVI(
        model=model,
        guide=guide,
        optim=Adam({"lr": lr}),
        loss=Trace_ELBO(num_particles=10)
    )

    elbo_history = []
    for step in range(num_iterations):
        elbo = -svi.step(x_tensor, y_tensor)
        elbo_history.append(elbo)

        if (step + 1) % 200 == 0:
            print(f"Step {step + 1}/{num_iterations} | ELBO: {-elbo:.2f}")

    return elbo_history, svi


if __name__ == "__main__":
    torch.manual_seed(42)
    N = 100
    X = torch.randn(N, 1)
    y = 1.5 * X.squeeze(-1) + 0.5 + torch.randn(N) * 0.3

    print("=" * 60)
    print("Variational Inference with Pyro — Bayesian Model")
    print("=" * 60)

    elbo_history, svi = train_pyro_model(X, y, num_iterations=1000, lr=0.005)

    print("\n" + "=" * 40)
    print("Variational parameters learned by optimization:")
    for name, value in pyro.get_param_store().items():
        print(f"  {name}: {value.item():.4f}")

    print("\nThe learned values should approximately correspond"
          " to the true regression y ≈ 1.5x + 0.5 ✅")

3. MCMC vs VI Comparison on a Linear Regression Problem

Let’s compare variational inference with MCMC (Markov Chain Monte Carlo) methods, which provide asymptotically exact posterior samples but are much more computationally expensive.

import torch
import pyro
import pyro.distributions as dist
from pyro.infer import MCMC, NUTS, SVI, Trace_ELBO
import numpy as np
import time

torch.manual_seed(42)
pyro.set_rng_seed(42)


def model_comparison(x, y=None):
    """Bayesian model for fair MCMC vs VI comparison."""
    w = pyro.sample("w", dist.Normal(0.0, 5.0).to_event(1))
    b = pyro.sample("b", dist.Normal(0.0, 5.0))
    sigma = pyro.sample("sigma", dist.HalfCauchy(2.0))

    mean = x @ w + b
    if y is not None:
        with pyro.plate("data", x.shape[0]):
            pyro.sample("obs", dist.Normal(mean, sigma), obs=y)


def guide_comparison(x, y=None):
    """Complete variational guide for MCMC/VI comparison."""
    w_loc = pyro.param("w_loc", torch.zeros(1))
    w_scale = pyro.param("w_scale", torch.ones(1),
                         constraint=dist.constraints.positive)
    pyro.sample("w", dist.Normal(w_loc, w_scale).to_event(1))

    b_loc = pyro.param("b_loc_cmp", torch.tensor(0.0))
    b_scale = pyro.param("b_scale_cmp", torch.tensor(1.0),
                         constraint=dist.constraints.positive)
    pyro.sample("b", dist.Normal(b_loc, b_scale))

    sigma_loc = pyro.param("sigma_loc", torch.tensor(1.0),
                           constraint=dist.constraints.positive)
    pyro.sample("sigma", dist.Normal(sigma_loc, torch.tensor(0.1)))


def compare_mcmc_vi(x_data, y_data):
    """Compares MCMC and VI in terms of accuracy and computation time."""
    print("\n" + "=" * 60)
    print("COMPARISON: MCMC (No-U-Turn) vs Variational Inference")
    print("=" * 60)

    # ─── MCMC with NUTS (exact reference) ───
    print("\n--- MCMC (No-U-Turn Sampler) — Reference method ---")
    nuts_kernel = NUTS(model_comparison)
    mcmc = MCMC(nuts_kernel, num_samples=500, warmup_steps=200)

    t0 = time.time()
    mcmc.run(x_data, y_data)
    mcmc_time = time.time() - t0
    mcmc_samples = mcmc.get_samples()

    print(f"MCMC time: {mcmc_time:.2f} seconds")
    print(f"  Weight w_mean = {mcmc_samples['w'].mean():.4f}")
    print(f"  Weight w_std  = {mcmc_samples['w'].std():.4f}")
    print(f"  Bias b_mean   = {mcmc_samples['b'].mean():.4f}")
    print(f"  Noise sigma   = {mcmc_samples['sigma'].mean():.4f}")

    # ─── Variational Inference ───
    print("\n--- Variational Inference (SVI with ELBO) ---")
    pyro.clear_param_store()

    svi = SVI(model_comparison, guide_comparison,
              pyro.optim.Adam({"lr": 0.005}),
              loss=Trace_ELBO(num_particles=10))

    t0 = time.time()
    for step in range(2000):
        svi.step(x_data, y_data)
    vi_time = time.time() - t0

    print(f"VI time: {vi_time:.2f} seconds")
    print(f"  Weight w_loc    = {pyro.param('w_loc').item():.4f}")
    print(f"  Weight w_scale  = {pyro.param('w_scale').item():.4f}")
    print(f"  Bias b_loc      = {pyro.param('b_loc_cmp').item():.4f}")
    print(f"  Noise sigma     = {pyro.param('sigma_loc').item():.4f}")

    # ─── Summary ───
    print("\n" + "-" * 60)
    print("COMPARATIVE SUMMARY — MCMC vs Variational Inference")
    print("-" * 60)
    print(f"{'Method':<25} {'Time (s)':<14} {'w_mean':<12} {'Type':<20}")
    print(f"{'MCMC (NUTS)':<25} {mcmc_time:<14.2f} "
          f"{mcmc_samples['w'].mean().item():<12.4f} "
          f"{'Exact sampling':<20}")
    print(f"{'VI (SVI / ELBO)':<25} {vi_time:<14.2f} "
          f"{pyro.param('w_loc').item():<12.4f} "
          f"{'Fast approximation':<20}")

    print(f"\nSpeed: VI is ~{mcmc_time/vi_time:.1f}x faster than MCMC")
    print("Accuracy: VI and MCMC give similar results ✓")


if __name__ == "__main__":
    N = 80
    X = torch.randn(N, 1)
    y = 1.5 * X.squeeze(-1) + 0.5 + torch.randn(N) * 0.3
    compare_mcmc_vi(X, y)

Key Hyperparameters

Variational inference has several critical hyperparameters that significantly influence the quality and speed of convergence of the approximation. Here are the most important ones to know and tune properly.

latent_dim — Dimension of the Latent Space

The dimension of the latent variables z corresponds to the number of parameters that the variational distribution q(z;ϕ) must estimate. It is the most fundamental and most difficult hyperparameter to choose correctly.

  • Typical values: from 2 for simple regression problems, up to several hundred for deep generative models like VAEs.
  • Impact: a latent_dim that is too small produces an overly restrictive approximation that cannot capture the complexity of the true posterior. An excessively large latent_dim increases the risk of overfitting and considerably lengthens optimization time.
  • Recommendation: start small (10-50) and increase progressively until quality metrics stop improving significantly.

learning_rate — Learning Rate

The learning rate controls the size of the update steps for the parameters ϕ during ELBO optimization by stochastic gradient descent.

  • Typical values: between 0.0001 and 0.05 depending on the optimizer used. With Adam (recommended by default), 0.001 to 0.01 generally gives good results.
  • Impact: a learning_rate that is too high causes oscillations and ELBO divergence. A learning_rate that is too low leads to extremely slow convergence and possible entrapment in poor local minima.
  • Recommendation: use a decreasing learning rate schedule (e.g., torch.optim.lr_scheduler.StepLR) or adaptive optimizers like Adam that automatically adjust the learning step.

num_samples — Monte Carlo Samples

The number of Monte Carlo samples used to estimate the expectation E_q in the ELBO computation. This is a direct trade-off between estimation accuracy and computational cost.

  • Typical values: between 1 and 50. In practice, 1 sample (the REINFORCE / score function estimator) can suffice for backpropagation thanks to the reparameterization trick, but 5-20 samples significantly reduce gradient variance.
  • Impact: few samples → noisy gradient but very fast computation. Many samples → precise estimate but considerably slowed training.
  • Recommendation: start with num_samples=10, then adjust according to the variability observed in the ELBO convergence curve. If the curve is very noisy, increase progressively.

num_steps — Number of Optimization Iterations

The total number of gradient descent steps to maximize the ELBO.

  • Typical values: between 1000 and 10000 depending on model complexity and data size.
  • Impact: too few steps → under-optimization (the ELBO has not reached its plateau). Too many steps → wasted computation time and potential risk of overfitting on small datasets.
  • Recommendation: visualize the ELBO convergence curve and stop when its progress becomes negligible (plateau). An early stopping criterion based on validation ELBO is preferable to a fixed number of steps.

Other Important Hyperparameters

  • batch_size: size of mini-batches for stochastic ELBO computation. Influences gradient variance and training speed.
  • constraint on scales (sigma): always use dist.constraints.positive in Pyro to guarantee that scale parameters remain strictly positive.
  • warmup_steps: number of burn-in iterations if using hybrid VI-MCMC methods.

Advantages and Limitations

Advantages of Variational Inference

Exceptional computational speed — Variational inference is typically one to two orders of magnitude faster than MCMC methods. Stochastic gradient optimization converges in a few hundred or thousands of steps, whereas NUTS requires thousands of samples with a much higher per-step cost. This is the decisive advantage for real-time applications or very large datasets.

Scalability to massive data — Thanks to stochastic ELBO computation on mini-batches, variational inference can handle millions of observations without issue. The entire dataset never needs to fit in memory simultaneously. This is fundamental for large-scale Bayesian deep learning.

Natural integration with neural networks — The reparameterization trick allows variational inference to be incorporated directly into any neural network architecture. This is the founding principle of Variational Autoencoders (VAEs) and many modern deep generative models. The entire pipeline — encoder, decoder, ELBO computation — is end-to-end differentiable.

Automatic uncertainty quantification — Unlike point estimation methods (MLE, MAP), variational inference produces a full distribution over latent variables. We obtain not only a central estimate but also a measure of epistemic uncertainty, essential for critical applications (medical, aerospace, finance).

Remarkable modeling flexibility — We can choose virtually any variational family: factorized Gaussian, Gaussian mixture, conditional distributions (normalizing flows), or even non-parametric distributions. This flexibility allows the approximation to be tailored to the specific complexity of each problem.

Limitations of Variational Inference

Inherent approximation bias — Variational inference systematically produces an approximation, never an exact solution. The KL(q||p) has a mode-covering bias: q tends to “undercover” the true posterior, ignoring secondary modes. In contexts where absolute accuracy is essential (rigorous scientific inference), MCMC methods remain preferable despite their slowness.

Difficulty with mean-field approximations — The assumption of independence between latent variables is violated in many realistic models. When variables exhibit significant correlations, the mean-field approximation produces systematic underestimations of uncertainty (variance too narrow). More expressive variational families (flows, copulas) can mitigate this problem at the cost of increased complexity.

Non-convex optimization and local minima — The ELBO is generally not convex with respect to the parameters ϕ, especially when the guide is a deep neural network. The risk of converging to poor local minima is real and significant. Careful initialization, multiple restarts, and adaptive learning rate schedules are generally necessary.

Delicate choice of the variational family — There is no universally optimal variational family. An overly restrictive family produces a poor, biased approximation; an overly expressive family leads to overfitting and numerical instability. This choice remains largely empirical and requires substantial domain expertise.

Sensitivity to hyperparameters — The performance of variational inference depends heavily on the learning rate, the number of Monte Carlo samples, the latent dimension, and many other choices. Unlike MCMC methods that asymptotically converge to the correct solution (under relatively mild conditions), variational inference can converge to a poor approximation if the hyperparameters are poorly tuned.

4 Concrete Use Cases of Variational Inference

Use Case 1: Deep Generative Models — Variational Autoencoders

This is the most famous and influential application of variational inference. A VAE combines an encoder (which learns a variational distribution q(z|x) over latent representations) with a decoder (which models p(x|z) to reconstruct the data).

import torch.nn as nn
import torch.nn.functional as F

class ToyVAE(nn.Module):
    """Minimal variational autoencoder for learning."""

    def __init__(self, input_dim=784, latent_dim=20, hidden_dim=400):
        super().__init__()
        # Variational encoder
        self.fc_encode = nn.Linear(input_dim, hidden_dim)
        self.fc_mu = nn.Linear(hidden_dim, latent_dim)
        self.fc_logvar = nn.Linear(hidden_dim, latent_dim)
        # Decoder
        self.fc_decode = nn.Linear(latent_dim, hidden_dim)
        self.fc_out = nn.Linear(hidden_dim, input_dim)

    def encode(self, x):
        h = F.relu(self.fc_encode(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):
        h = F.relu(self.fc_decode(z))
        return torch.sigmoid(self.fc_out(h))

    def forward(self, x):
        mu, logvar = self.encode(x)
        z = self.reparameterize(mu, logvar)
        return self.decode(z), mu, logvar

    def loss_function(self, recon_x, x, mu, logvar):
        """ELBO = reconstruction - KL divergence."""
        reconstruction_loss = F.binary_cross_entropy(
            recon_x, x, reduction='sum'
        )
        kl_divergence = -0.5 * torch.sum(
            1 + logvar - mu.pow(2) - logvar.exp()
        )
        return reconstruction_loss + kl_divergence

VAEs have revolutionized the generation of images, music, and text by enabling the learning of continuous and structured latent representations.

Use Case 2: Bayesian Regression with Quantified Uncertainty

In medical, financial, or industrial applications where decisions have significant consequences, it is crucial to know not only the prediction but also the associated degree of certainty. Variational inference naturally provides this uncertainty quantification.

A VI regression model assigns a probability distribution to each model coefficient, rather than a single point value. This allows the construction of rigorous confidence intervals and the detection of regions of the input space where the model is uncertain (areas where training data is scarce).

Use Case 3: Latent Variable Models for Recommendation

Modern recommendation systems frequently use latent variable models to capture the underlying preferences of users. Variational inference allows these models to be trained efficiently on massive preference matrices that would be inaccessible to classical MCMC methods.

The variational distribution over user latent factors also makes it possible to estimate the confidence in each recommendation. If a user has little history, their variational distribution will be more diffuse, judiciously reflecting the increased uncertainty.

Use Case 4: Approximate Inference in Bayesian Graphical Models

Bayesian graphical models (Bayesian networks, Markov random fields) are ubiquitous in natural language processing, bioinformatics, and computer vision. Variational inference provides a unified and scalable framework for learning and inference in these complex models.

Rather than exponentially enumerating all possible configurations of hidden variables, variational inference approximates the joint distribution with a tractable factorized distribution. The mean-field approximation is particularly popular in this context because it enables analytical coordinate updates for many exponential family distributions.

See Also

About Salah YAHIAOUI

Salah YAHIAOUI publie sur The Pingouin des tutoriels Python et des décryptages sur l’intelligence artificielle. Les cas pratiques récents précisent leurs données, leurs tests et leurs limites. À propos, méthode éditoriale et contact.

View all posts by Salah YAHIAOUI →