EasyVAE

VAE Encoder

Auto-Encoding Variational Bayes (VAE)

Easy

Problem

Implement the inference network of a variational autoencoder. The encoder maps each input row to the mean and log-variance of a diagonal Gaussian latent distribution.

\mu = XW_{\mu} + b_{\mu}

\log \sigma^2 = XW_{\log \sigma^2} + b_{\log \sigma^2}

Here, X \in \mathbb{R}^{B \times D} is a batch of inputs, W_{\mu} and W_{\log \sigma^2} have shape (D,L), the two biases have shape (L), and L is the latent width. Return a Python dictionary with exactly two keys: mu and log_var. Each value must be a float64 NumPy array with shape (B,L).

Theory

The encoder, or recognition model, is the component of a Variational Autoencoder (VAE) that maps an input x to the parameters of an approximate posterior distribution q_\phi(z|x). Rather than producing a single latent point, it outputs two vectors -- a mean \mu and a log-variance \log \sigma^2 -- that together define a Gaussian distribution over latent space. This design was introduced by Kingma and Welling in "Auto-Encoding Variational Bayes" (2014).


What It Is

The VAE encoder is a neural network parameterized by weights \phi that takes a data point x and produces the sufficient statistics of a diagonal Gaussian distribution in latent space. It outputs two vectors:

These outputs come from two separate linear projections from a shared representation. In the simplest form -- a linear encoder with no hidden layers -- the encoder takes raw input x \in \mathbb{R}^{d_x} and computes:

\mu = x W_\mu, \quad \log \sigma^2 = x W_{\log\sigma^2}

where W_\mu \in \mathbb{R}^{d_x \times d_z} and W_{\log\sigma^2} \in \mathbb{R}^{d_x \times d_z} are independent weight matrices, and d_z is the latent dimension. The encoder returns the tuple (\mu, \log \sigma^2), used downstream for sampling via the reparameterization trick and for computing the KL divergence.


Key Equations

The encoder defines the approximate posterior as a diagonal Gaussian:

q_\phi(z|x) = \mathcal{N}(z; \mu_\phi(x), \text{diag}(\sigma_\phi^2(x)))

The two linear projections:

\mu = x W_\mu \in \mathbb{R}^{d_z}

\log \sigma^2 = x W_{\log\sigma^2} \in \mathbb{R}^{d_z}

Given these parameters, a latent sample is drawn using the reparameterization trick (a separate component):

z = \mu + \sigma \odot \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)

where \sigma = \exp(\frac{1}{2} \log \sigma^2) and \odot denotes element-wise multiplication. The encoder's job ends at producing (\mu, \log \sigma^2); the sampling step is downstream.


Why Output Distribution Parameters, Not Points

A standard autoencoder maps each input to a single point: z = f(x). The VAE encoder instead maps each input to a distribution. This is not an arbitrary choice -- it is required by variational inference. The true posterior p(z|x) is itself a distribution, but computing it requires an intractable integral over the entire latent space. The encoder's distribution q_\phi(z|x) is a tractable approximation optimized to be close to the true posterior.

Outputting a distribution serves three essential purposes:

The tension between reconstruction accuracy (narrow posteriors) and regularization (posteriors close to the prior) is the fundamental trade-off in VAE training, controlled by the relative weight of the reconstruction loss and the KL term.


Why Log-Variance Instead of Variance

The encoder outputs \log \sigma^2 rather than \sigma^2 directly. This is a critical design choice with both numerical and optimization motivations.

The constraint problem: Variance must be strictly positive (\sigma^2 > 0). A linear layer can output any real number, including zero or negative values. Enforcing positivity with softplus or ReLU + epsilon introduces saturation or dead zones.

The log-variance solution: \log \sigma^2 ranges over (-\infty, +\infty), matching a linear layer's natural output range. The variance is recovered as:

\sigma^2 = \exp(\log \sigma^2)

Since \exp(\cdot) maps any real number to a strictly positive value, positivity of \sigma^2 is guaranteed by construction.

Numerical range advantages:

KL divergence simplification: The log-variance appears directly in the KL formula for a diagonal Gaussian against a standard normal:

D_{KL} = -\frac{1}{2} \sum_{j=1}^{d_z} \left(1 + \log \sigma_j^2 - \mu_j^2 - \sigma_j^2 \right)

Here \log \sigma_j^2 is used directly from the encoder's output, and \sigma_j^2 = \exp(\log \sigma_j^2) appears only once. This avoids \log(\exp(\cdot)) roundtrips that can cause numerical instability.


The Two Projections

The mean and log-variance are computed by two completely separate linear projections sharing no weights. From input x (or hidden representation h), the two projections operate independently:

\mu = h W_\mu + b_\mu

\log \sigma^2 = h W_{\log\sigma^2} + b_{\log\sigma^2}

Why separate weights? The two outputs encode fundamentally different information:

Shared weights would force a rigid coupling between location and uncertainty, preventing the encoder from independently adjusting where a point maps and how certain it is about that mapping.

Independent learning dynamics: The reconstruction loss primarily shapes W_\mu (pushing means toward latent locations that reconstruct well), while the KL term shapes both W_\mu and W_{\log\sigma^2} (pushing the distribution toward the prior). Separate weights let each projection specialize without interference.

Shapes: Both projections output vectors of size d_z, so W_\mu and W_{\log\sigma^2} have the same shape. The encoder's total output is the tuple (\mu, \log \sigma^2), a 2 \times d_z-dimensional description of the approximate posterior.


Paper Context: Kingma and Welling (2014)

In "Auto-Encoding Variational Bayes," the encoder is called the recognition model: "We introduce a recognition model q_\phi(z|x): an approximation to the intractable true posterior p_\theta(z|x)." The key contribution is showing this recognition model can be trained jointly with the decoder by optimizing the ELBO:

\mathcal{L}(\theta, \phi; x) = \mathbb{E}_{q_\phi(z|x)}[\log p_\theta(x|z)] - D_{KL}(q_\phi(z|x) \| p(z))

The first term measures reconstruction quality under encoder samples; the second is KL divergence against the prior. The encoder's parameters \phi appear in both -- it must balance useful codes for reconstruction with staying close to the prior.

Before the VAE, variational inference required deriving custom update equations per model, often relying on conjugate priors. The VAE showed that a neural network recognition model plus the reparameterization trick makes variational inference general and scalable via SGD. The paper demonstrates this on MNIST and Frey Face using MLPs with the two-projection architecture.


The Amortized Inference Insight

Traditional variational inference optimizes separate variational parameters for each data point -- N separate (\mu_i, \log \sigma_i^2) pairs for N observations. This per-datapoint inference scales poorly.

The VAE encoder performs amortized inference: a single network with shared parameters \phi maps any input x to its variational parameters. Instead of O(N \times d_z) variational parameters, you have O(|\phi|) network parameters that generalize across all inputs.

Practical consequences:

The trade-off is the amortization gap: one set of weights for all inputs means the encoder may not perfectly optimize parameters for any individual input. In practice, this gap is small and the efficiency gains are enormous. Kingma and Welling state: "the variational parameters \phi are not optimized per datapoint but rather are shared across data points, hence amortizing the cost of inference."


Numerical Example

Consider a minimal VAE encoder with input dimension d_x = 3 and latent dimension d_z = 2, with no hidden layers -- just two linear projections.

Input vector:

x = [1.0, \; 0.5, \; -0.5]

Weight matrix for \mu:

W_\mu = \begin{pmatrix} 0.2 & -0.1 \\ 0.4 & 0.3 \\ -0.3 & 0.5 \end{pmatrix}

Weight matrix for \log \sigma^2:

W_{\log\sigma^2} = \begin{pmatrix} 0.1 & 0.0 \\ -0.2 & 0.6 \\ 0.3 & -0.4 \end{pmatrix}

Computing \mu

\mu = x W_\mu:

\mu_1 = (1.0)(0.2) + (0.5)(0.4) + (-0.5)(-0.3) = 0.2 + 0.2 + 0.15 = 0.55

\mu_2 = (1.0)(-0.1) + (0.5)(0.3) + (-0.5)(0.5) = -0.1 + 0.15 - 0.25 = -0.2

\mu = [0.55, \; -0.2]

Computing \log \sigma^2

\log \sigma^2 = x W_{\log\sigma^2}:

(\log \sigma^2)_1 = (1.0)(0.1) + (0.5)(-0.2) + (-0.5)(0.3) = 0.1 - 0.1 - 0.15 = -0.15

(\log \sigma^2)_2 = (1.0)(0.0) + (0.5)(0.6) + (-0.5)(-0.4) = 0.0 + 0.3 + 0.2 = 0.5

\log \sigma^2 = [-0.15, \; 0.5]

Interpreting the Output

The encoder returns (\mu, \log \sigma^2) = ([0.55, -0.2], \; [-0.15, 0.5]), defining a 2D diagonal Gaussian:

Latent dimension 1:

Latent dimension 2:

The first dimension has negative log-variance, giving variance below 1.0 (tighter than the prior). The second has positive log-variance, giving variance above 1.0 (wider than the prior). Both are valid -- \exp(\cdot) guarantees positivity regardless of sign.

The approximate posterior for this input is:

q_\phi(z|x) = \mathcal{N}\left(z; \begin{pmatrix} 0.55 \\ -0.2 \end{pmatrix}, \begin{pmatrix} 0.861 & 0 \\ 0 & 1.649 \end{pmatrix} \right)

The zero off-diagonal entries reflect the diagonal Gaussian assumption -- each latent dimension is independent given the input.


Pitfalls


Examples

Example 1

Input
x = [[0.5,-0.3,0.1],[0.2,0.8,-0.5]], W_mu = [[0.1,-0.2],[0.3,0.1],[-0.1,0.4]], b_mu = [0,0.1], W_logvar = [[-0.2,0.1],[0.1,-0.3],[0.2,0]], b_logvar = [0.05,-0.05]
Output
{"mu":[[-0.05,0.01],[0.31,-0.06]],"log_var":[[-0.06,0.09],[-0.01,-0.27]]}
Explanation
The two supplied affine projections produce the Gaussian mean and log-variance independently.

Example 2

Input
x = [[1,-1],[0.5,0.25]], W_mu = [[0.4],[-0.2]], b_mu = [0.1], W_logvar = [[0.1],[0.3]], b_logvar = [-0.2]
Output
{"mu":[[0.7],[0.25]],"log_var":[[-0.4],[-0.075]]}

Example 3

Input
x = [[-0.4,0.6,0.2]], W_mu = [[0.2,0.1],[-0.3,0.5],[0.4,-0.2]], b_mu = [-0.1,0.2], W_logvar = [[0.1,-0.4],[0.2,0.3],[-0.5,0.2]], b_logvar = [0,0.1]
Output
{"mu":[[-0.28,0.42]],"log_var":[[-0.02,0.48]]}

Hints

  1. Use one matrix multiplication for each latent parameter.
  2. Broadcast each bias across the batch dimension.

Requirements

Constraints

Starter Code

import numpy as np

def vae_encoder(x: np.ndarray, W_mu: np.ndarray, b_mu: np.ndarray,
                W_logvar: np.ndarray, b_logvar: np.ndarray) -> dict:
    """
    Returns mu and log_var as float64 arrays in a dictionary.
    """
    pass

Test Cases

CaseMatches
Two samplespublic
Single latent coordinatepublic
Single samplepublic