VAE Decoder
Auto-Encoding Variational Bayes (VAE)
Easy
Problem
Implement a Bernoulli variational-autoencoder decoder. Project each latent vector into the data width, add the supplied bias, and apply a sigmoid so every reconstructed feature lies between zero and one.
\widehat{X} = \sigma(ZW_{\mathrm{dec}} + b_{\mathrm{dec}})
\sigma(a) = \frac{1}{1+e^{-a}}
Here, Z \in \mathbb{R}^{B \times L} contains latent samples, W_{\mathrm{dec}} \in \mathbb{R}^{L \times D} is the decoder weight matrix, and b_{\mathrm{dec}} \in \mathbb{R}^{D} is the decoder bias. Return \widehat{X} as a float64 NumPy array with shape (B,D).
Theory
The decoder is the generative half of a Variational Autoencoder. Given a latent code z sampled from the latent space, the decoder maps it back to the data space to produce a reconstruction \hat{x}. In Kingma and Welling (2014), this is the learned conditional distribution $$, the component that gives VAEs their generative power.
What It Is
The VAE decoder takes a low-dimensional latent vector z and transforms it into a reconstruction \hat{x} that lives in the original data space. If the input data has dimensionality D (for example, a flattened 28x28 MNIST image has $ = 784$) and the latent space has dimensionality L (typically much smaller, such as L = 2 or L = 10), the decoder is a function f: \mathbb{R}^L \to \mathbb{R}^D that expands the compressed representation back to full size.
In probabilistic terms, the decoder parameterizes the conditional distribution p_\theta(x|z). Given a specific latent code z, it outputs the parameters of a distribution over data x. For continuous data with a Gaussian output assumption, the decoder outputs the mean \mu_{x|z} of a Gaussian, and the reconstruction loss becomes the mean squared error between the decoder output and the true data point.
This problem implements the simplest possible decoder: a single linear transformation with no activation function. The paper uses deeper, nonlinear networks, but the linear version isolates the core concept of mapping from latent space to data space without the complexity of hidden layers.
Key Equations
The Reconstruction
The decoder computes the reconstruction as a matrix multiplication between the latent vector and a weight matrix:
\hat{x} = z \cdot W
where z \in \mathbb{R}^{B \times L} is the batch of latent vectors, W \in \mathbb{R}^{L \times D} is the decoder weight matrix, and \hat{x} \in \mathbb{R}^{B \times D} is the batch of reconstructed outputs.
The Generative Model p_\theta(x|z)
In the probabilistic framework of Kingma and Welling (2014), the decoder defines a conditional likelihood. For continuous data with a Gaussian assumption:
p_\theta(x|z) = \mathcal{N}(x; \mu_\theta(z), \sigma^2 I)
where \mu_\theta(z) = z \cdot W is the decoder's output and \sigma^2 is a fixed variance. The negative log-likelihood of this Gaussian reduces to the mean squared error:
-\log p_\theta(x|z) \propto \|x - \hat{x}\|^2 = \|x - zW\|^2
Minimizing MSE is equivalent to maximizing the log-likelihood under the Gaussian output assumption. This is why MSE loss is the standard reconstruction loss for VAEs with continuous data.
The Generative Model
The decoder is where the "generative" in "generative model" comes from. The encoder compresses data into latent codes, but the decoder is the component that can produce new data. Once trained, the decoder can take any point z in the latent space and produce a plausible data point \hat{x}.
The generative process works in two stages. First, sample a latent code from the prior distribution z \sim p(z) = \mathcal{N}(0, I). Second, pass this sampled z through the decoder to get \hat{x} = f_\theta(z). The quality of generation depends entirely on how well the decoder has learned to map from the latent space to the data space.
During training, the decoder receives z values produced by the encoder via the reparameterization trick: z = \mu + \epsilon \odot \sigma where \epsilon \sim \mathcal{N}(0, I). The KL divergence term in the ELBO loss regularizes the encoder's output distribution to stay close to \mathcal{N}(0, I), ensuring that the latent space is structured in a way that the decoder can generalize to unseen z values at generation time.
Why Linear
This problem uses a single linear transformation \hat{x} = zW with no activation function and no hidden layers. This is a deliberate simplification that isolates the decoder's essential role: spatial expansion from latent dimension to data dimension.
A linear decoder can only learn linear relationships between the latent space and the data space. If the true data distribution requires nonlinear mappings (as virtually all real-world data does), a linear decoder will produce blurry, low-quality reconstructions. It captures principal directions of variation, similar to PCA, but cannot model curved manifolds or sharp transitions.
The original paper uses multi-layer neural networks with nonlinear activations for both the encoder and decoder. In the MNIST experiments, Kingma and Welling use a 500-unit hidden layer with tanh activation followed by a sigmoid output layer. The sigmoid constrains outputs to [0, 1], matching pixel intensity range. The linear-only version here removes these layers to focus on the core latent-to-data mapping.
Decoder as Inverse of Encoder
The encoder compresses data from \mathbb{R}^D down to \mathbb{R}^L. The decoder expands from \mathbb{R}^L back to \mathbb{R}^D. These are conceptual inverses, but not mathematical inverses.
If the encoder applies z = xW_{\text{enc}} and the decoder applies \hat{x} = zW_{\text{dec}}, the full reconstruction is \hat{x} = xW_{\text{enc}}W_{\text{dec}}. For \hat{x} = x to hold, we would need W_{\text{enc}}W_{\text{dec}} = I_D. But the product W_{\text{enc}}W_{\text{dec}} \in \mathbb{R}^{D \times D} has rank at most L < D, so perfect reconstruction is impossible when L < D. The decoder finds the best approximation, not an exact inverse.
The encoder and decoder have separate, independently learned weight matrices. Nothing in the training procedure forces W_{\text{dec}} = W_{\text{enc}}^T. Some autoencoder variants (called tied-weight autoencoders) enforce this relationship, but the standard VAE does not. Each matrix is free to learn whatever mapping minimizes the combined reconstruction and KL loss.
The asymmetry extends to the probabilistic interpretation. The encoder approximates the posterior q_\phi(z|x), while the decoder defines the likelihood p_\theta(x|z). The encoder learns to infer latent codes that explain observed data; the decoder learns to generate data that matches observed examples.
The Latent Space
The latent space is the low-dimensional continuous space that the decoder reads from. Its properties directly determine the quality and usefulness of the decoder's output.
- Low-dimensional: The latent space typically has far fewer dimensions than the data space. For MNIST (D = 784), L = 2 or L = 10 is common. This compression forces the model to learn a compact representation capturing only the most important factors of variation.
- Continuous: Unlike discrete representations such as cluster assignments, the VAE latent space is continuous. Small movements in latent space produce small changes in the decoder output. Moving from z = [0.5, 0.3] to z = [0.6, 0.3] produces a subtle change in reconstruction, not a sudden jump.
- Smooth: Points nearby in latent space produce similar outputs. This enables interpolation: given two latent codes z_1 and z_2, decoding points along z_t = (1-t)z_1 + tz_2 for t \in [0, 1] produces a smooth transition between the two reconstructions.
- Regularized: The KL divergence term prevents the encoder from mapping data to narrow, isolated regions. Without regularization, large gaps would appear where the decoder has never seen any z value, causing generation from those gaps to produce garbage. The KL term ensures the latent space is densely covered.
Paper Context
Kingma and Welling (2014) introduce the VAE in "Auto-Encoding Variational Bayes." The decoder (generative model) is one of three core components, alongside the encoder (recognition model) and the reparameterization trick.
The paper states: "The generative model p_\theta(x|z), also called the decoder, learns to reconstruct the input from the latent representation." The decoder outputs the parameters of a distribution over x, and the reconstruction loss derives from the negative log-likelihood of this distribution.
The decoder appears in the ELBO objective:
\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 is the expected reconstruction log-likelihood, which the decoder directly controls. Maximizing this term pushes the decoder to produce reconstructions that match the input. The decoder's weights \theta only appear in the first term; the second term regularizes the encoder.
The paper emphasizes that "the true posterior p_\theta(z|x) is intractable" when the decoder is a neural network with nonlinear hidden layers. This intractability motivates the variational approach: computing the exact posterior requires integrating over all possible z values weighted by the decoder likelihood, and this integral has no closed-form solution for nonlinear decoders.
When the decoder output is treated as the mean of a Gaussian p_\theta(x|z) = \mathcal{N}(x; \mu_\theta(z), I), the log-likelihood simplifies to a negative squared error. For binary data like binarized MNIST, a Bernoulli likelihood with sigmoid output and binary cross-entropy loss is used instead.
Numerical Example
Consider a decoder with latent dimension L = 2 and output dimension D = 4. The weight matrix W has shape (2, 4).
Setup
W = \begin{bmatrix} 0.3 & -0.1 & 0.5 & 0.2 \\ -0.2 & 0.4 & 0.1 & -0.3 \end{bmatrix}
and a single latent vector z = \begin{bmatrix} 1.0 & -0.5 \end{bmatrix}.
Computing the Reconstruction
The reconstruction is \hat{x} = z \cdot W, a matrix multiplication of shape (1, 2) \times (2, 4) = (1, 4). For each output dimension j, compute the dot product of z with column j of W:
\hat{x}_0 = 1.0 \times 0.3 + (-0.5) \times (-0.2) = 0.3 + 0.1 = 0.4
\hat{x}_1 = 1.0 \times (-0.1) + (-0.5) \times 0.4 = -0.1 - 0.2 = -0.3
\hat{x}_2 = 1.0 \times 0.5 + (-0.5) \times 0.1 = 0.5 - 0.05 = 0.45
\hat{x}_3 = 1.0 \times 0.2 + (-0.5) \times (-0.3) = 0.2 + 0.15 = 0.35
Result
\hat{x} = \begin{bmatrix} 0.4 & -0.3 & 0.45 & 0.35 \end{bmatrix}
A 2-dimensional latent vector has been expanded to a 4-dimensional reconstruction. Each output value is a linear combination of the two latent dimensions, weighted by the corresponding column of W.
Batch Example
With two latent vectors z = \begin{bmatrix} 1.0 & -0.5 \\ 0.0 & 1.0 \end{bmatrix}, the first row produces [0.4, -0.3, 0.45, 0.35] as above. The second row with z = [0, 1] outputs [-0.2, 0.4, 0.1, -0.3], which is simply the second row of W. Each row of W acts as a basis vector in the data space, and the latent coordinates z specify how to combine these basis vectors.
Generation
The entire point of the VAE framework is generation: producing new data points that resemble the training distribution but are not copies of any training example. The decoder makes this possible.
The Generative Process
Sample from the prior: draw z \sim \mathcal{N}(0, I) in the latent space
Decode: compute \hat{x} = z \cdot W
Output: \hat{x} is the generated data point
No encoder is needed at generation time. The encoder's role is purely during training, where it maps data points to latent codes for the decoder to learn from. Once trained, the decoder operates independently with random z samples.
Why the Prior Matters
The choice of prior p(z) = \mathcal{N}(0, I) is critical. During training, the KL divergence term pushes the encoder's output distribution toward \mathcal{N}(0, I), so the decoder sees approximately standard-normally distributed z values. At generation time, sampling z \sim \mathcal{N}(0, I) produces inputs from the same distribution the decoder was trained on, ensuring reasonable outputs.
If the encoder were allowed to map training data to arbitrary, tightly clustered regions (which would happen without the KL term), random samples from \mathcal{N}(0, I) would land in regions the decoder has never seen, producing nonsensical output. The KL regularization ensures the decoder's training distribution matches its generation distribution.
Pitfalls
Wrong weight dimensions. The decoder weight matrix must have shape (L, D) where L is the latent dimension and D is the output dimension. A common mistake is transposing this to (D, L), which attempts to multiply a (B, L) latent matrix by a (D, L) weight, causing a shape mismatch. The weight's first dimension must match z's last dimension.
Confusing encoder and decoder weights. The encoder weight matrix has shape (D, L) (data-to-latent), the decoder has shape (L, D) (latent-to-data). These are separate learned parameters. Using the encoder's weight matrix in the decoder produces outputs with the wrong dimensionality.
Applying activation when not specified. This problem specifies a linear decoder with no activation function. Adding ReLU, sigmoid, or tanh changes the output range and introduces nonlinearity. A sigmoid clamps outputs to [0, 1], which might seem appropriate for image data but is incorrect here. The output should be in (-\infty, +\infty).
Generating from the wrong distribution. When using the decoder for generation, latent vectors must be sampled from \mathcal{N}(0, I). Sampling from a uniform distribution or a Gaussian with non-unit variance produces z values outside the decoder's training distribution. For example, z \sim \mathcal{N}(0, 10I) produces latent vectors with magnitudes roughly 10 times larger than training, leading to wildly incorrect reconstructions.
Forgetting the batch dimension. The decoder should handle batched inputs where z has shape (B, L). Passing a 1D vector of shape (L,) without reshaping to (1, L) can produce output with the wrong shape or cause broadcasting errors.
Expecting perfect reconstruction. Because L < D, the decoder cannot perfectly reconstruct the input. The encoding is lossy, and the decoder can only recover a projection onto the L-dimensional subspace. Exact equality between input and reconstruction will always fail; tolerance-based comparison is needed.
Examples
Example 1
- Input
z = [[0.5,-0.3],[1,0.2]], W_dec = [[0.4,-0.2,0.1],[0.3,0.5,-0.4]], b_dec = [0.1,-0.1,0.2]- Output
[[0.552308,0.413382,0.591459],[0.636453,0.450166,0.554779]]- Explanation
- The affine decoder logits are converted into Bernoulli feature probabilities by sigmoid.
Example 2
- Input
z = [[-1,0.5],[0,2]], W_dec = [[0.6],[-0.2]], b_dec = [0.05]- Output
[[0.34299],[0.413382]]
Example 3
- Input
z = [[0,0,0]], W_dec = [[0.1,0.2],[-0.3,0.4],[0.5,-0.2]], b_dec = [-0.4,0.7]- Output
[[0.401312,0.668188]]
Hints
- Broadcast b_dec across the rows of z @ W_dec.
- Apply np.exp inside the sigmoid expression.
Requirements
- Use NumPy.
- Apply the supplied affine decoder projection.
- Apply sigmoid elementwise to the projected values.
- Return a float64 NumPy array with shape (B, D).
Constraints
- z has shape (B, L) and dtype float64.
- W_dec has shape (L, D) and dtype float64.
- b_dec has shape (D) and dtype float64.
- B, L, and D are positive.
Starter Code
import numpy as np
def vae_decoder(z: np.ndarray, W_dec: np.ndarray, b_dec: np.ndarray) -> np.ndarray:
"""
Returns the float64 reconstruction with shape (B, D).
"""
passTest Cases
| Case | Matches | |
|---|---|---|
| Two reconstructions | — | public |
| Single output coordinate | — | public |
| Zero latent vector | — | public |