Mean vector, covariance matrix, the bivariate density, and sampling any Gaussian from standard noise with a matrix square root, with the PyTorch parametrization used in the first fitting experiments.
A generative model is a distribution we can sample from and, ideally, evaluate (What generative modeling is). Before neural networks enter, the course works with the simplest distribution that is still interesting in several dimensions: the multivariate Gaussian. It is the running example of the first notes, where it is both the target we draw data from and the model we fit to that data. It also comes back in almost every later model: the prior of a VAE, the noise of a diffusion model, the base distribution of a normalizing flow and the feature statistics behind FID.
The opening block of the course has three steps:
generate data from a known distribution (this note);
fit a parametrized 2-D distribution to the data by gradient descent on such a distance (Fitting distributions with KL).
Notation. Capital letters such as X and Z denote random vectors; bold lowercase letters such as x and z denote points of the plane, the values those vectors take. Bold capitals such as Σ, A and L are matrices, I is the identity matrix, A⊤ the transpose and ∣Σ∣ the determinant. We write X∼N(μ,σ2) for a univariate normal with mean μ and variance σ2, and X∼N(μ,Σ) for a multivariate normal with mean vector μ and covariance matrix Σ: the second argument is always the variance or covariance, never the standard deviation. Densities are written p and q; from the PyTorch section on, P is the fixed target distribution with density p and Q the trainable model with density q.
where ρ is the correlation coefficient between X1 and X2. The matrix is symmetric, because Cov(X1,X2)=Cov(X2,X1). It is also positive semidefinite: for any fixed vector a the variance Var(a⊤X)=a⊤Σa cannot be negative. Three free numbers hold every variance and covariance of the vector.
The example used throughout this note is a Gaussian centred at the origin with
μ=(00),Σ=(10.50.51):
unit variances and correlation ρ=0.5, so the cloud leans along the diagonal x1=x2.
Two variances describe how far the cloud spreads along each coordinate axis. That is enough only when the coordinates are uncorrelated, and then the cloud is stretched along the axes. The off-diagonal entry records what the variances cannot: how the coordinates move together. It tilts the cloud.
In Figure 1 the right cloud is the left one rotated by 45°. The two clouds have the same eigenvalues (1.5 and 0.5), but both axis variances of the right cloud equal 1. Those are exactly the axis variances of the round N(0,I) cloud, so two variances alone cannot tell the tilted cloud from a circle. Only Cov(X1,X2)=0.5 records the tilt.
Figure
A diagonal matrix only stretches along the axes; covariance tilts the cloud
Both clouds are drawn from the same standard-normal pairs (260, minus the few that fall outside either window). Left: Σ = diag(1.5, 0.5), independent coordinates, so the spread is described by Var(X₁) and Var(X₂) alone. Right: Σ = [[1, 0.5], [0.5, 1]], the running example. Its eigenvalues are 1.5 and 0.5, so it is the left cloud rotated by 45°. Its variances along the axes are both 1, and only the off-diagonal entry records the tilt. Braces span ±1 standard deviation, ±√Var, and dashed guides drop to them from where the 1σ ellipse touches its extremes; solid and dashed curves are the 1σ and 2σ ellipses.
When Σ is invertible (positive definite, not just semidefinite), the joint density on the whole plane R2 is
p(x)=2π∣Σ∣1×exp(−21(x−μ)⊤Σ−1(x−μ)),
where x=(x1,x2)⊤, Σ−1 is the inverse and ∣Σ∣ the determinant of the covariance matrix. The shapes in the quadratic form are
1×2(x−μ)⊤2×2Σ−12×1(x−μ),
so the exponent is a scalar. In d dimensions the normalizing constant becomes (2π)−d/2∣Σ∣−1/2, and d=2 gives the expression above (Bishop, 2006, §2.3).
The level sets of the density, where the squared Mahalanobis distance(x−μ)⊤Σ−1(x−μ) equals k2, are ellipses (Murphy, 2022, §3.2). Figures 1 and 3 draw them as the 1σ (k=1) and 2σ (k=2) contours. The probability inside such an ellipse follows from the map of the sampling section below. Writing X=μ+Σ1/2Z with Z∼N(0,I), and using that Σ1/2 is symmetric with Σ1/2Σ−1Σ1/2=I, the squared distance becomes Z⊤Z=Z12+Z22, the squared length of standard noise. The standard density 2π1e−r2/2 depends only on the radius r, so in polar coordinates
Pr(Z12+Z22≤k2)=∫0kre−r2/2dr=1−e−k2/2.
This is the CDF of the χ2 distribution with two degrees of freedom, evaluated at k2. So the 1σ ellipse holds 1−e−1/2≈39% of the probability and the 2σ ellipse 1−e−2≈86%, not the 68% and 95% of one-dimensional intervals.
In short. To sample N(μ,Σ), draw Z∼N(0,I) and return μ+AZ for any matrix A with AA⊤=Σ; in code A is the Cholesky factor. The rest of this section builds the square root and proves the recipe twice, by the change of variables and by the moments. Readers who know affine maps of Gaussians from the Probability course can treat those two proofs as optional and go straight to the worked example.
In one dimension, standardization and its inverse are
So to sample any normal distribution it is enough to sample the standard one, stretch by σ and shift by μ.
Figure
Standardization and its inverse in one dimension
The point z = 1 of the standard normal maps to x = μ + σz = 3.5 for μ = 2, σ = 1.5; dividing (x − μ) by σ maps it back. Both points sit one standard deviation above their own mean.
The bivariate case needs a matrix that plays the role of σ.
Spectral decomposition. By the spectral theorem, every real symmetric matrix, and in particular every covariance matrix, can be written as
Σ=UΛU⊤,
where Λ is diagonal with the eigenvalues of Σ and the columns of the orthogonal matrix U are the matching normalized eigenvectors. The eigenvalues are non-negative because Σ is positive semidefinite, so we can define the matrix square root
Σ1/2=UΛ1/2U⊤,
where Λ1/2 holds the square roots of the eigenvalues. The root is itself symmetric, because (UΛ1/2U⊤)⊤=UΛ1/2U⊤. Since U⊤U=I, we get Σ1/2Σ1/2=UΛU⊤=Σ.
Transformation. If Z∼N(0,I), then
X=μ+Σ1/2Z∼N(μ,Σ).
More generally, any square matrix A with AA⊤=Σ works; Σ1/2 is one, because it is symmetric and squares to Σ. For positive definite Σ such an A is invertible, since (detA)2=∣Σ∣>0, and the change-of-variables formula gives the density of X=μ+AZ (Murphy, 2022, §2.8). We substitute z=A−1(x−μ) into the standard density 2π1e−z⊤z/2 and divide by ∣detA∣=∣Σ∣1/2:
because z⊤z=(x−μ)⊤(A−1)⊤A−1(x−μ) and (A−1)⊤A−1=(AA⊤)−1=Σ−1. This is the density of N(μ,Σ) from the previous section. (For a singular Σ the same map defines a degenerate Gaussian concentrated on a line, which has no density on the plane.) The first two moments give a quick check. The mean is μ+AE[Z]=μ, and the covariance is
Cov(μ+AZ)=ACov(Z)A⊤=AIA⊤=Σ.
The symmetric root Σ1/2 is one choice. For positive definite Σ there is exactly one lower-triangular L with positive diagonal and LL⊤=Σ, the Cholesky factor (Golub and Van Loan, 2013, §4.2). It is the usual choice in code: a finite algorithm computes it in about d3/3 floating-point operations for a d×d matrix, while an eigendecomposition needs an iterative method. Unlike the symmetric root, it requires positive definiteness: torch.linalg.cholesky fails on a singular covariance, and MultivariateNormal accepts only a positive definite covariance_matrix. PyTorch’s MultivariateNormal samples exactly this way: it stores the Cholesky factor of the covariance it receives and returns μ+LZ with Z∼N(0,I) (PyTorch documentation and source). The same pattern, noise in and a deterministic transformation out, is the reparameterization trick of the VAE and the one-step noising formula of DDPM.
Figure
Sampling any Gaussian from standard noise
Independent standard normal pairs Z are multiplied by a matrix A with AAᵀ = Σ (here the Cholesky factor L of the running example's Σ = [[1, 0.5], [0.5, 1]]) and then shifted by μ = (1.5, 1). The amber point is one example point z = (2, 1) followed through both steps: it moves to Az = (2, 1.87) and then to μ + Az = (3.5, 2.87). Curves are 1σ and 2σ contours; all panels share the same window.
For Σ=(10.50.51) the characteristic equation is (1−λ)2=0.25, so the eigenvalues are λ1=1.5 and λ2=0.5. The eigenvectors are 21(1,1)⊤ and 21(1,−1)⊤. The variance is largest along the diagonal x1=x2: the 1σ ellipse has semi-axes 1.5≈1.225 along it and 0.5≈0.707 across it. Two valid square roots are
Check: 0.9662+0.2592≈1 and 2⋅0.966⋅0.259≈0.5. For the Cholesky factor, LL⊤=(10.50.50.25+0.75)=Σ. Both matrices turn independent standard noise into samples of the same correlated cloud.
Check yourself. Take Σ=(1−0.8−0.81). Along which diagonal does the cloud stretch, and what is its Cholesky factor? Answer: the eigenvalues are 1.8 and 0.2, the long axis runs along x1=−x2 with semi-axes 1.8≈1.342 and 0.2≈0.447, and L=(1−0.800.6), since LL⊤=(1−0.8−0.80.64+0.36)=Σ.
torch.distributions contains parametrizable distributions with methods to evaluate their (log-)density and to draw samples. The 2-D Gaussian is MultivariateNormal. The fitting experiments in Fitting distributions with KL need a Gaussian whose parameters an optimizer can change freely. The covariance makes that non-trivial: an arbitrary 2×2 matrix is generally not a valid covariance.
The fix is to learn the Cholesky factor instead of Σ. With
L=(eab0ec),LL⊤=(e2abeabeab2+e2c),
the three raw numbers a,b,c can take any real values. The diagonal of L is stored through a logarithm, so it stays positive and the triangular matrix L is invertible. Then LL⊤ is symmetric and positive definite: for v=0, v⊤LL⊤v=∥L⊤v∥2>0 because L⊤ is invertible. The running example is reached with a=0, b=0.5 and c=ln0.75≈−0.144; the initialization a=b=c=0 gives Σ=I.
Parameters that should be learned are created with requires_grad=True, which tells PyTorch’s autograd to record operations on them and compute their gradients. The model distribution Q has a trainable mean and trainable raw entries of L. The target distribution P uses requires_grad=False, because it stays fixed. Figure 4 shows the starting point of the first fitting experiment, with each cloud coloured by its own density.
Figure
Target P and model Q before fitting
400 samples each of the fixed target P = N((−5, −5), I) and of the trainable model Q, initialized at N(0, I), on the plot limits (−10, 10). Each dot is coloured by its own cloud’s density, p(x) for target samples and q(x) for model samples, so stronger colour means higher density.
A minimal sketch of both ways to sample the running example:
import torchfrom torch import distributions as distrsmu = torch.zeros(2)cov = torch.tensor([[1.0, 0.5], [0.5, 1.0]])target = distrs.MultivariateNormal( mu, covariance_matrix=cov)# 1000 samples, shape (1000, 2)x = target.sample(torch.Size([1000]))# L = [[1, 0], [0.5, 0.866]]L = torch.linalg.cholesky(cov)# the same distribution, by handx_manual = mu + torch.randn(1000, 2) @ L.T
Each row of torch.randn(1000, 2) is one draw z⊤, so @ L.T computes (Lz)⊤ row by row.
The practical part of this note follows the Distances between distributions notebook from the HSE-LAMBDA Deep Generative Models course repository. The link is pinned to a fixed commit, and cell numbers count every cell from 1.
Cell 2 sets up the environment: it imports Matplotlib with the fivethirtyeight style, NumPy, PyTorch with torch.distributions as distrs, and IPython’s clear_output, which the training loops later use to redraw their plots.
Cell 4 defines plot_2d_dots, a scatter plot of an N×2 array on the default limits (−10,10) for both axes (arguments xlim and ylim), one small dot per sample, coloured by an optional value array through the colour map cmap (default viridis).
Cell 5 defines the three helpers behind the parametrization above. get_parameters(mu, sigma, requires_grad) creates the mean (μ,μ) and the raw entries a=c=lnσ, b=0. create_distr assembles L and returns MultivariateNormal(mu, L @ L.T), which passes LL⊤ as covariance_matrix. sample(distr, n) returns an n×2 tensor. Since L is already a valid Cholesky factor, MultivariateNormal(mu, scale_tril=L) defines the same distribution and skips the re-factorization that PyTorch performs on L @ L.T; the PyTorch documentation recommends scale_tril for this reason.
Cells 8–11 create the fixed target N((−5,−5),I) and the trainable model Q initialized at N(0,I), and plot both clouds coloured by p(x) and q(x) (Figure 4). Cells 8 and 11 draw 1000 samples from each distribution, and the plots use the viridis and plasma colour maps; Figure 4 shows 400 samples per cloud and encodes density by colour strength instead. The fit itself is the subject of Fitting distributions with KL.
With a target distribution and a trainable model in hand, the remaining question is what “fit” means: we need a number that says how far Q is from P. The first candidate is the KL divergence, and to understand it we start from entropy in Entropy, cross-entropy and KL divergence.
K. P. Murphy. Probabilistic Machine Learning: An Introduction. MIT Press, 2022. §2.8: transformations of random variables, including the change-of-variables formula; §3.2: the multivariate Gaussian and the Mahalanobis distance.
G. H. Golub, C. F. Van Loan. Matrix Computations, 4th ed. Johns Hopkins University Press, 2013. §4.2: positive definite systems and the Cholesky factorization.