Course contents 01 / 33

01 / 33 · Foundations

What generative modeling is

The problem the course solves, learning to produce new data from examples of an unknown distribution, why it is hard, how a computer turns uniform random numbers into samples of any distribution, and how VAEs, GANs, flows and diffusion models each attack the problem.

Prerequisites

Probability · Calculus ↗

On this page 10

A classifier answers a question about an object: is this image a cat? A generative model produces the object itself: a new image of a cat that was not in the training set. This note sets up what that means precisely, why it is hard, and how the rest of the course attacks it.

The problem: examples of an unknown distribution#

We treat a data set as independent draws from some distribution:

x(1),…,x(N)∼pdata(x).x^{(1)}, \dots, x^{(N)} \sim p_{\text{data}}(x).

For images, xx is a vector of pixel values; for text, a sequence of tokens; for audio, a waveform. The distribution pdatap_{\text{data}} is unknown: nobody can write down the probability density of “photographs of cats”. All we have is the sample.

The goal is a model pθ(x)p_\theta(x) with parameters θ\theta such that pθ≈pdatap_\theta \approx p_{\text{data}}, and from which we can draw new samples x∼pθx \sim p_\theta. If the approximation is good, the new samples look like the data without copying it.

Figure

Generative modeling: from examples to a model and back to examples

Data, a mixture model and new samplesThree panels. First: a cloud of points shaped as two interleaved half-moons, labelled p_data is unknown. An arrow labelled fit theta leads to the second: 16 pairs of concentric rings along the same half-moons, the components of a Gaussian mixture. An arrow labelled sample leads to the third: a new cloud of points with the same shape.datax⁽¹⁾, …, x⁽ᴺ⁾ ~ pdatamodelpθ(x) ≈ pdata(x)new samplesx ~ pθpdata is unknownfit θsampleData, a mixture model and new samplesThree panels. First: a cloud of points shaped as two interleaved half-moons, labelled p_data is unknown. An arrow labelled fit theta leads to the second: 16 pairs of concentric rings along the same half-moons, the components of a Gaussian mixture. An arrow labelled sample leads to the third: a new cloud of points with the same shape.datax⁽¹⁾, …, x⁽ᴺ⁾ ~ pdatamodelpθ(x) ≈ pdata(x)new samplesx ~ pθpdata is unknownfit θsample
Left: 300 points from a distribution whose formula we do not know (two noisy half-circles). Middle: a model pθ, here an equal-weight mixture of 16 Gaussians with σ = 0.12 placed along the data; the rings are the 1σ and 2σ contours of each component. It illustrates what fitting produces rather than the result of an actual fit. Right: 300 new samples from the model; they look like the data without repeating any original point.

So yes: generative modeling is learning to sample from a distribution we do not know, by approximating it from examples. Two questions follow. What does “approximate” mean, so that we can train? And how does a computer sample from a distribution at all? The rest of the note answers both.

What a generative model is for#

Sampling new objects is the most visible use, but not the only one:

  • Generation. New images, text, audio, molecules that resemble the training data.
  • Conditional generation. A model of p(x∣c)p(x \mid c) produces data that matches a condition cc: an image for a text prompt, the missing part of a photograph, a high-resolution version of a small image, a translation of a sentence.
  • Density estimation. If the model can evaluate pθ(x)p_\theta(x), the density is one possible anomaly score: low density can flag unusual objects. The score depends on the representation (a density changes under a change of units or variables) and on how well the model fits, and a high density at a point is neither a high probability of that point nor a guarantee that it looks typical; the anomaly detection note discusses these limits. Good density models are also good compressors, since an optimal code spends about −log⁡2p(x)-\log_2 p(x) bits on xx.
  • Representation learning. Models with a latent variable (the VAE) map data to a low-dimensional code whose directions often carry meaning, learned without labels.

What “approximate” means: a divergence to minimize#

To train pθp_\theta we need a number that says how far it is from pdatap_{\text{data}}. The standard choice is the Kullback–Leibler divergence (Entropy, cross-entropy and KL divergence):

DKL(pdata ∥ pθ)=Ex∼pdata[log⁡pdata(x)]−Ex∼pdata[log⁡pθ(x)].D_{\mathrm{KL}}(p_{\text{data}} \,\|\, p_\theta) = \mathbb{E}_{x \sim p_{\text{data}}}\big[\log p_{\text{data}}(x)\big] - \mathbb{E}_{x \sim p_{\text{data}}}\big[\log p_\theta(x)\big].

The first term does not depend on θ\theta. Minimizing the divergence therefore means maximizing the expected log-likelihood of the data under the model, and with the sample in place of the expectation this is maximum likelihood:

θ⋆=arg⁡max⁡θ1N∑i=1Nlog⁡pθ(x(i)).\theta^\star = \arg\max_\theta \frac{1}{N}\sum_{i=1}^{N} \log p_\theta\big(x^{(i)}\big).

The catch is that this needs pθ(x)p_\theta(x) as a number. For flexible models that is often impossible to compute, and the families in this course differ mainly in how they get around it: an exact formula, a lower bound, or a different objective altogether.

The course starts with this objective on the simplest case: a 2-D Gaussian fitted to Gaussian data by gradient descent on the KL divergence (Fitting distributions with KL).

How a computer samples at all#

A model is only generative if we can draw from it. Computers do not draw from arbitrary distributions directly. Everything is built from one primitive, uniform random numbers, by a chain of transformations. The whole section is one idea:

Start with a distribution that is easy to sample, and transform its samples into samples of a harder one.

The chain has five links, and every later model rests on them, so we go through them slowly. Each step gives the idea, the key formula in a frame, why it works, and the same step in a few lines of code. Readers who already know inverse-CDF and Box–Muller sampling can skip to why this is hard and the six families.

Step 1: uniform numbers#

A computer cannot flip a coin. A processor is deterministic: the same program on the same input does the same thing every time. Random numbers in programs come from two sources.

  • Physical noise. The operating system collects events nobody can predict, such as the exact timing of keyboard, network and disk interrupts and electrical noise inside the processor, and mixes them into a pool that programs read (/dev/urandom on Linux and macOS, os.urandom in Python). These bits are truly unpredictable, but they are slow to produce. They are used for cryptographic keys and for picking a starting point, not for the millions of numbers a simulation needs.
  • Pseudo-random generators. A short deterministic rule turns one number into the next. The generator keeps an internal state ss, and each call updates it, sn+1=T(sn)s_{n+1} = T(s_n), and returns a number computed from it. The whole sequence is determined by the starting state, which is set from a seed. A good generator passes large published batteries of statistical tests; no generator passes every conceivable test, since a test that knows the rule can always predict the next number.

A toy generator. The simplest rule is the linear congruential generator: multiply, add, keep the remainder (Knuth, 1997, §3.2.1). With 16 possible states,

sn+1=(5 sn+3) mod 16,un=sn/16.s_{n+1} = (5\,s_n + 3) \bmod 16, \qquad u_n = s_n / 16 .

Start from the seed s0=7s_0 = 7. Then 5⋅7+3=38=2⋅16+65 \cdot 7 + 3 = 38 = 2 \cdot 16 + 6, so s1=6s_1 = 6. Next, 5⋅6+3=33=2⋅16+15 \cdot 6 + 3 = 33 = 2 \cdot 16 + 1, so s2=1s_2 = 1. Continuing the same way gives the states

7, 6, 1, 8, 11, 10, 5, 12, 15, 14, 9, 0, 3, 2, 13, 4, 7, …7,\ 6,\ 1,\ 8,\ 11,\ 10,\ 5,\ 12,\ 15,\ 14,\ 9,\ 0,\ 3,\ 2,\ 13,\ 4,\ 7,\ \dots

and dividing by 16 gives the numbers 0.4375,0.375,0.0625,0.5,0.6875,…0.4375, 0.375, 0.0625, 0.5, 0.6875, \dots After 16 steps the generator is back at 7 and repeats. On the way it has visited every state exactly once, so over one cycle each of the 16 values 0,116,…,15160, \tfrac1{16}, \dots, \tfrac{15}{16} appears exactly once: no value is preferred. That is what “uniform” means for a generator.

Figure

Where the "random" numbers come from

The cycle of a toy generator and pairs of consecutive numbersSixteen circles around a ring labelled with the states 7, 6, 1, 8, 11, 10, 5, 12, 15, 14, 9, 0, 3, 2, 13, 4, joined by arrows around the ring; circle 7 is highlighted. Two squares: the first holds 16 points on parallel slanted lines, the second 400 evenly scattered points.states of the toy generator7618111051215149032134seed 7toy: pairs (uₙ, uₙ₊₁)uₙ01a good generator: 400 pairsuₙ01The cycle of a toy generator and pairs of consecutive numbersSixteen circles around a ring labelled with the states 7, 6, 1, 8, 11, 10, 5, 12, 15, 14, 9, 0, 3, 2, 13, 4, joined by arrows around the ring; circle 7 is highlighted. Two squares: the first holds 16 points on parallel slanted lines, the second 400 evenly scattered points.states of the toy generator7618111051215149032134seed 7toy generatoruₙ01good: 400 pairsuₙ01
Left: the toy generator s ← (5s + 3) mod 16 started from the seed 7. It visits all 16 states, 7 → 6 → 1 → 8 → …, and returns to 7, so over one cycle every value u = s/16 occurs exactly once. Right: pairs of consecutive numbers (uₙ, uₙ₊₁). For the toy generator they lie on a few parallel lines, because each number is tied to the previous one by a simple rule; for a modern generator 400 pairs fill the square with no visible pattern.

What real generators change. The toy has two visible defects. It has only 16 values and repeats after 16 steps, and each number is tied to the previous one by a simple rule, so consecutive pairs (un,un+1)(u_n, u_{n+1}) fall on a few straight lines. Real generators keep the idea of a state updated by a fixed rule and remove both defects.

  • An enormous state. PCG64, NumPy’s default, has a 128-bit state and repeats only after 21282^{128} numbers (O’Neill, 2014). The Mersenne Twister, PyTorch’s generator on the CPU, has a 19,937-bit state and a period of 219937−12^{19937} - 1 (Matsumoto and Nishimura, 1998).
  • A scrambling output step. The state is mixed by bit shifts, rotations and multiplications before it is returned, so no pattern shows between consecutive numbers. PCG64 passes BigCrush, the largest battery of the TestU01 library, and the Mersenne Twister fails only its two tests of linear complexity (L’Ecuyer and Simard, 2007).
  • Counters on GPUs. Thousands of GPU threads need numbers at once, so PyTorch uses Philox there. It computes the nn-th number directly as a scrambled function of nn and the seed, so threads do not have to wait for each other (Salmon et al., 2011).

From an integer to a number in [0,1)[0, 1). A generator returns 64-bit integers k∈{0,1,…,264−1}k \in \{0, 1, \dots, 2^{64} - 1\}. A double-precision number has a 53-bit mantissa, so libraries keep the top 53 bits of kk and scale them:

u=⌊k/211⌋⋅2−53.u = \big\lfloor k / 2^{11} \big\rfloor \cdot 2^{-53} .

The result is one of 2532^{53} equally spaced values in [0,1)[0, 1), each equally likely. With seed 0, NumPy’s first raw output is k=11 749 869 230 777 074 271k = 11\,749\,869\,230\,777\,074\,271, and the formula gives u=0.63696…u = 0.63696\ldots, exactly the first number that np.random.default_rng(0).random() returns.

The seed. The seed sets the starting state; the library expands a small integer into the full state. The same seed, with the same generator, the same calls with the same arguments in the same order and the same build of the library, gives the same numbers, which is what makes an experiment repeatable. That is a narrow guarantee. NumPy keeps the raw streams of its generators stable across versions, but the samplers built on them, such as normal or multivariate_normal, may change between feature releases or builds (NumPy documentation), and a whole GPU training run adds sources of variation of its own (a training run in practice). Different seeds do not automatically give independent streams either: in the toy generator above every seed lands on the same cycle of 16 states, so another seed only shifts the same sequence. Large generators make overlaps very unlikely, and for parallel streams NumPy derives child seeds with SeedSequence.spawn (NumPy documentation).

In code. The library does all of the above in one call:

import numpy as np
rng = np.random.default_rng(0)       # seed 0 fixes the whole sequence
u = rng.random(100_000)              # 100,000 numbers from U(0, 1)
print(u[:3].round(4))                # [0.637  0.2698 0.041 ]
print((u < 0.25).mean())             # 0.25015: a quarter of them fall below 0.25

Step 2: any one-dimensional distribution, by inverting the CDF#

A discrete example first. Suppose we need an outcome aa, bb or cc with probabilities 0.20.2, 0.50.5 and 0.30.3. Cut the interval [0,1)[0, 1) into three pieces of exactly these lengths, [0,0.2)[0, 0.2), [0.2,0.7)[0.2, 0.7) and [0.7,1)[0.7, 1). Then draw uu and return the outcome whose piece contains it. A uniform number falls into a piece with probability equal to the piece’s length, so each outcome comes out with the probability we asked for. For u=0.637u = 0.637, the first number above, the result is bb.

Figure

Sampling a discrete distribution: cut the interval [0, 1)

The interval [0, 1) cut into three pieces, the staircase CDF and the shares of outcomesA vertical strip from 0 to 1 split into pieces of length 0.2, 0.5 and 0.3 for outcomes a, b and c. A staircase CDF with steps at 0.2, 0.7 and 1. A dashed line from u = 0.637 runs right to the b step and down to outcome b. Bars of the shares in 10,000 draws, near 0.2, 0.5 and 0.3.abc00.20.71uabcF (running total)u = 0.63710,000 draws: shares0.200a0.503b0.297cThe interval [0, 1) cut into three pieces, the staircase CDF and the shares of outcomesA vertical strip from 0 to 1 split into pieces of length 0.2, 0.5 and 0.3 for outcomes a, b and c. A staircase CDF with steps at 0.2, 0.7 and 1. A dashed line from u = 0.637 runs right to the b step and down to outcome b. Bars of the shares in 10,000 draws, near 0.2, 0.5 and 0.3.abc00.20.71uabcF (running total)u = 0.63710,000 draws: shares0.200a0.503b0.297c
We need outcome a, b or c with probabilities 0.2, 0.5 and 0.3. The interval [0, 1) is cut into pieces of exactly these lengths at 0.2 and 0.7, the running totals of the probabilities, which form the CDF F (the staircase in the middle). The uniform number u = 0.637 falls into the second piece, so the result is b. Right: the shares of the outcomes in 10,000 draws, 0.200, 0.503, 0.297; the ticks mark 0.2, 0.5 and 0.3.

The cut points 0.20.2, 0.70.7 and 11 are the running totals of the probabilities, Pr⁡(outcome≤a)\Pr(\text{outcome} \le a), Pr⁡(outcome≤b)\Pr(\text{outcome} \le b) and Pr⁡(outcome≤c)\Pr(\text{outcome} \le c). That is, they are the values of the cumulative distribution function FF. Sampling is a lookup: find the first outcome whose running total exceeds uu.

The continuous case is the same idea with infinitely many pieces. For a continuous random variable the CDF F(x)=Pr⁡(X≤x)F(x) = \Pr(X \le x) rises smoothly from 0 to 1: it says how much probability lies to the left of xx. Assume it is continuous and strictly increasing, so that every level uu is reached at exactly one point. Here is the trick: draw a uniform uu and ask which value of xx has accumulated exactly that much probability. That value is the sample:

 X=F−1(U)∼F where F−1(u) is the x with F(x)=u.\boxed{\,X = F^{-1}(U) \sim F\,} \qquad \text{where } F^{-1}(u) \text{ is the } x \text{ with } F(x) = u .

This is inverse transform sampling (Devroye, 1986, §II.2; the functions of random variables note of the probability course treats it formally).

Why it works. Fix any number xx and compute the CDF of the returned value in three lines:

Pr⁡(X≤x)=Pr⁡(F−1(U)≤x)=Pr⁡(U≤F(x))=F(x).\begin{aligned} \Pr(X \le x) &= \Pr\big(F^{-1}(U) \le x\big) \\ &= \Pr\big(U \le F(x)\big) \\ &= F(x) . \end{aligned}

The first line is the definition of XX. The second holds because FF is increasing: the point where the CDF reaches uu lies at or left of xx exactly when uu is at most F(x)F(x). The third holds because a uniform number is at most F(x)F(x) with probability equal to the length of [0,F(x)][0, F(x)]. So XX has CDF FF: exactly the distribution we wanted.

Worked example: the exponential distribution. Waiting times with rate λ\lambda have the CDF F(x)=1−e−λxF(x) = 1 - e^{-\lambda x} for x≥0x \ge 0. Solving F(x)=uF(x) = u for xx takes three steps:

1−e−λx=u,e−λx=1−u,−λx=ln⁡(1−u), x=−ln⁡(1−u)λ \begin{aligned} 1 - e^{-\lambda x} &= u, \\ e^{-\lambda x} &= 1 - u, \\ -\lambda x &= \ln(1 - u), \end{aligned} \qquad \boxed{\,x = -\frac{\ln(1 - u)}{\lambda}\,}

With λ=1\lambda = 1:

uu0.10.50.90.99
x=−ln⁡(1−u)x = -\ln(1 - u)0.1050.6932.3034.605

Half of the uniform numbers (u<0.5u < 0.5) land in the short stretch [0,0.693)[0, 0.693), and the last 1% are spread over everything beyond 4.6. The shape of the exponential, many short waits and a long tail of long ones, appears by itself.

In code. One line turns uniform numbers into exponential ones (log1p(-u) computes ln⁡(1−u)\ln(1 - u) accurately for small uu):

import numpy as np
u = np.random.default_rng(0).random(100_000)
x = -np.log1p(-u)                                 # x = -ln(1 - u): exponential with rate 1
print(x.mean().round(3), np.median(x).round(3))   # 0.998 0.691 (theory: 1 and ln 2 = 0.693)

The normal distribution. For the standard normal, FF is Φ(x)=12(1+erf⁡(x/2))\Phi(x) = \tfrac12\big(1 + \operatorname{erf}(x/\sqrt2)\big). The recipe is the same, but Φ−1\Phi^{-1} has no formula in elementary functions. Libraries compute it with rational approximations accurate to about 16 digits, for example Wichura’s algorithm (Wichura, 1988). The figure shows why the recipe produces a bell: equal stretches of uu land on short stretches of xx where Φ\Phi is steep and on long ones where it is flat.

Figure

From uniform to normal through the inverse CDF

Inverse transform sampling: u from U(0, 1) becomes x = Φ⁻¹(u)Top plot: the S-shaped curve Φ(x) rising from 0 to 1 over x from −3.5 to 3.5. Six values of u are marked on the vertical axis: 0.06, 0.24, 0.42, 0.58, 0.8 and 0.95. From each a dashed line runs right to the curve and then down to the x axis. Bottom plot: a histogram of 4000 values of Φ⁻¹(u) with the bell-shaped N(0, 1) density drawn over it.00.51Φ(x)u ~ U(0, 1)−3−2−10123x4000 values of Φ⁻¹(u)N(0, 1) densityInverse transform sampling: u from U(0, 1) becomes x = Φ⁻¹(u)Top plot: the S-shaped curve Φ(x) rising from 0 to 1 over x from −3.5 to 3.5. Six values of u are marked on the vertical axis: 0.06, 0.24, 0.42, 0.58, 0.8 and 0.95. From each a dashed line runs right to the curve and then down to the x axis. Bottom plot: a histogram of 4000 values of Φ⁻¹(u) with the bell-shaped N(0, 1) density drawn over it.00.51Φ(x)u ~ U(0, 1)−3−2−10123x4000 values of Φ⁻¹(u)N(0, 1) density
Top: the standard normal CDF Φ. Six uniform numbers u on the vertical axis go across to the curve and down to the x axis: x = Φ⁻¹(u). Equal stretches of u map to short stretches of x where Φ is steep (near zero) and to long ones in the tails, so the values of x pile up in the middle. Bottom: a histogram of 4000 such values (bin width 0.25) with the N(0, 1) density on top; they agree.

Step 3: the standard normal, by Box–Muller#

Inverting Φ\Phi numerically works, but it is slow. Box and Muller found an exact recipe that turns two independent uniforms U1,U2U_1, U_2 into two independent standard normals (Box and Muller, 1958). First build a distance and an angle,

R=−2ln⁡U1,φ=2πU2,R = \sqrt{-2\ln U_1}, \qquad \varphi = 2\pi U_2,

then convert them to coordinates:

Z1=Rcos⁡φ,Z2=Rsin⁡φ.\boxed{\begin{aligned} Z_1 &= R\cos\varphi, \\ Z_2 &= R\sin\varphi . \end{aligned}}

The recipe looks arbitrary, but it is step 2 applied in polar coordinates. Think of (Z1,Z2)(Z_1, Z_2) as a random point in the plane, and describe it by its angle Θ\Theta and its distance RR from the origin.

  1. The angle is uniform. Two independent standard normals have the joint density 12πe−z12/2e−z22/2=12πe−r2/2\frac{1}{2\pi}e^{-z_1^2/2}e^{-z_2^2/2} = \frac{1}{2\pi}e^{-r^2/2}, where r2=z12+z22r^2 = z_1^2 + z_2^2. It depends only on the distance from the origin, so the cloud of points is circularly symmetric. Every direction is equally likely, so Θ\Theta is uniform on [0,2π)[0, 2\pi), and it does not depend on RR. One uniform number gives the angle: Θ=2πU2\Theta = 2\pi U_2, the φ\varphi above.

  2. The squared distance is exponential. The probability that the point lands within distance t\sqrt t of the origin is the integral of the density over that disc. In polar coordinates the area element is r dr dθr\,dr\,d\theta, and the integral over the angle gives 2π2\pi, which cancels the 12π\frac{1}{2\pi}:

    Pr⁡(R2≤t)=∫02π ⁣ ⁣∫0t12π e−r2/2 r dr dθ=[−e−r2/2]0t=1−e−t/2.\Pr(R^2 \le t) = \int_0^{2\pi}\!\!\int_0^{\sqrt t} \frac{1}{2\pi}\, e^{-r^2/2}\, r\,dr\,d\theta = \Big[-e^{-r^2/2}\Big]_0^{\sqrt t} = 1 - e^{-t/2} .

    This is the exponential CDF from the worked example, with rate λ=12\lambda = \tfrac12.

  3. Sample the squared distance by step 2. The worked example gives R2=−ln⁡(1−U1)/12=−2ln⁡(1−U1)R^2 = -\ln(1 - U_1)/\tfrac12 = -2\ln(1 - U_1). The number 1−U11 - U_1 is uniform too, so we may use U1U_1 itself: R2=−2ln⁡U1R^2 = -2\ln U_1 and R=−2ln⁡U1R = \sqrt{-2\ln U_1}. (The distance RR itself has the Rayleigh distribution.) In exact arithmetic U1=0U_1 = 0 has probability zero, but a floating-point generator on [0,1)[0, 1) can return exactly 0, where the logarithm is −∞-\infty; the code below therefore feeds the logarithm 1−U11 - U_1, which lies in (0,1](0, 1].

  4. Go back to coordinates. Z1=Rcos⁡ΘZ_1 = R\cos\Theta and Z2=Rsin⁡ΘZ_2 = R\sin\Theta, which is the boxed formula.

Worked example. Take u1=0.3u_1 = 0.3 and u2=0.8u_2 = 0.8.

  • Distance: R2=−2ln⁡0.3=2.408R^2 = -2\ln 0.3 = 2.408, so R=1.552R = 1.552. As a check, Pr⁡(R2≤2.408)=1−e−1.204=0.7=1−u1\Pr(R^2 \le 2.408) = 1 - e^{-1.204} = 0.7 = 1 - u_1.
  • Angle: Θ=2π⋅0.8\Theta = 2\pi \cdot 0.8, that is 288∘288^\circ, with cos⁡Θ=0.309\cos\Theta = 0.309 and sin⁡Θ=−0.951\sin\Theta = -0.951.
  • Point: z=(1.552⋅0.309, 1.552⋅(−0.951))=(0.480,−1.476)z = (1.552 \cdot 0.309,\ 1.552 \cdot (-0.951)) = (0.480, -1.476).
Figure

Box–Muller step by step: two uniform numbers → a point of the normal cloud

The Box–Muller transform in three panelsPanel one: 600 points filling the unit square evenly, with the point (0.3, 0.8) highlighted. Panel two: a ray from the origin at angle 288° and length 1.552, with an arc marking the angle. Panel three: a cloud of 600 normal points around zero with circles of radius 1 and 2 and the point (0.480, −1.476) highlighted.1. two uniform numbers(0.3, 0.8)u₁u₂12–3. angle and distanceangle 2π·0.8 = 288°2distance √(−2 ln 0.3) = 1.55234. a point in the planez = (0.480, −1.476)600 such points4The Box–Muller transform in three panelsPanel one: 600 points filling the unit square evenly, with the point (0.3, 0.8) highlighted. Panel two: a ray from the origin at angle 288° and length 1.552, with an arc marking the angle. Panel three: a cloud of 600 normal points around zero with circles of radius 1 and 2 and the point (0.480, −1.476) highlighted.1. two uniform numbers(0.3, 0.8)u₁u₂12–3. angle and distanceangle 2π·0.8 = 288°2distance √(−2 ln 0.3) = 1.55234. a point in the planez = (0.480, −1.476)600 such points4
Left: pairs of uniform numbers (u₁, u₂) in the unit square, with the example (0.3, 0.8) highlighted. Middle: the same pair in polar coordinates: u₂ sets the angle 2πu₂ and u₁ sets the distance √(−2 ln u₁). Right: the resulting points z, a cloud centred at zero, with circles of radius 1 and 2. The smaller u₁, the farther the point: u₁ near zero gives the rare points in the tails.

In code. The recipe gives standard deviation 1 in both coordinates and no correlation between them. In practice we call the library’s own normal sampler, which uses a faster variant of the same principle (NumPy’s uses the ziggurat method):

import numpy as np
rng = np.random.default_rng(1)
u1, u2 = 1 - rng.random(100_000), rng.random(100_000)   # 1 - U is in (0, 1]: log never sees 0
r, phi = np.sqrt(-2 * np.log(u1)), 2 * np.pi * u2
z1, z2 = r * np.cos(phi), r * np.sin(phi)
print(z1.std().round(3), z2.std().round(3), np.corrcoef(z1, z2)[0, 1].round(3))   # 0.998 1.002 -0.002
print((np.abs(z1) < 1).mean().round(3))   # 0.685: the familiar 68% within one standard deviation

Step 4: any Gaussian, by an affine map#

Steps 1–3 give standard normal noise Z∼N(0,I)Z \sim \mathcal N(\mathbf 0, \mathbf I): a round cloud centred at zero. A Gaussian with any mean μ\boldsymbol\mu and covariance Σ\boldsymbol\Sigma comes from it by a shift and a linear map. Take any matrix A\mathbf A with AA⊤=Σ\mathbf A\mathbf A^{\top} = \boldsymbol\Sigma; then

 X=μ+AZ∼N(μ,Σ) \boxed{\,X = \boldsymbol\mu + \mathbf A Z \sim \mathcal N(\boldsymbol\mu, \boldsymbol\Sigma)\,}

The mean shifts the cloud, and the matrix stretches, rotates and correlates its coordinates. The next note, Multivariate Gaussian distributions, shows how to choose A\mathbf A and why it works.

In code. The Cholesky factor is one valid choice of A\mathbf A:

import numpy as np
mu = np.array([1.0, 2.0])
Sigma = np.array([[2.0, 0.8], [0.8, 1.0]])
A = np.linalg.cholesky(Sigma)                 # lower triangular, A @ A.T == Sigma
z = np.random.default_rng(2).standard_normal((100_000, 2))
x = mu + z @ A.T
print(A.round(3).ravel())                     # [1.414 0.    0.566 0.825]
print(x.mean(axis=0).round(2), np.cov(x.T).round(2).ravel())   # [1. 2.] [2.  0.8 0.8 1. ]

Step 5: anything else, by a learned nonlinear map#

Gaussians are only ellipsoids, while images, speech and text have distributions far more complicated than an ellipse. To reach the shape of real data we push the noise through a nonlinear map gg:

 Z∼N(0,I)  ⟶  X=gθ(Z) \boxed{\,Z \sim \mathcal N(\mathbf 0, \mathbf I) \;\longrightarrow\; X = g_\theta(Z)\,}

The distribution of XX is called the pushforward of the noise distribution through gg. In a generative model, g=gθg = g_\theta is a neural network with parameters θ\theta, and training chooses θ\theta so that the pushforward matches the data.

Figure

Sampling as a chain of transformations of noise

Four point clouds: uniform noise, standard normal, a Gaussian, a bent distributionFour square panels joined by arrows. First: points filling the unit square evenly. An arrow labelled Box–Muller leads to the second: a round cloud centred at zero. An arrow labelled mu plus A z leads to the third: the same cloud stretched horizontally and squeezed vertically. An arrow labelled g leads to the fourth: the cloud bent into an arch. One highlighted point is marked in all four panels.uniform noiseu ~ U(0, 1)²standard normalz ~ N(0, I)any Gaussianμ + Az ~ N(μ, AAᵀ)complex shapex = g(μ + Az)Box–Mullerμ + A zg(·)Four point clouds: uniform noise, standard normal, a Gaussian, a bent distributionFour square panels joined by arrows. First: points filling the unit square evenly. An arrow labelled Box–Muller leads to the second: a round cloud centred at zero. An arrow labelled mu plus A z leads to the third: the same cloud stretched horizontally and squeezed vertically. An arrow labelled g leads to the fourth: the cloud bent into an arch. One highlighted point is marked in all four panels.uniform noiseu ~ U(0, 1)²standard normalz ~ N(0, I)any Gaussianμ + Az ~ N(μ, AAᵀ)complex shapex = g(μ + Az)Box–Mullerμ + A zg(·)
The same 320 pairs of uniform numbers go through every step; the amber point is one pair followed to the end. Box–Muller turns the square into a round standard normal cloud. The affine map μ + Az with A = diag(1.4, 0.45) and μ = 0 gives a Gaussian with covariance AAᵀ. The nonlinear map g(y) = (y₁, y₂ − 0.45 y₁² + 0.9) bends the ellipse into an arch, a distribution that is no longer Gaussian. In a generative model the last step is a trained network gθ.

This is the central picture of the course: a generative model is simple noise plus a learned transformation. For a direct generator, sampling is easy by construction (draw noise, apply the network once); some families below pay more for each sample, for example a chain of many network evaluations. The hard part is the other direction: choosing θ\theta, which needs some way to compare the pushforward with the data.

When gg is invertible and smooth, the density of XX even has a formula, the change of variables

pX(x)=pZ(g−1(x)) ∣det⁡∂g−1(x)∂x∣,p_X(x) = p_Z\big(g^{-1}(x)\big)\,\left|\det \frac{\partial g^{-1}(x)}{\partial x}\right|,

which is the starting point of normalizing flows. For a general network gθg_\theta there is no such formula, and that is where the families diverge.

Why this is hard#

Inverting a CDF solves sampling in one dimension, and a Gaussian has a formula in any dimension. Real data has neither.

  • Dimension. A 256×256256 \times 256 colour image has 256⋅256⋅3=196,608256 \cdot 256 \cdot 3 = 196{,}608 numbers. A density on that space cannot be tabulated, and there is no tractable general inverse-sampling procedure. A joint CDF exists, but it has no single inverse; sampling coordinate by coordinate would need every conditional CDF, which is exactly what we do not know.
  • Concentration. Natural images fill a tiny part of that space, close to a much lower-dimensional surface (a manifold): almost every random pixel array is noise. The model has to put nearly all its probability on that thin set.
  • Normalization. A flexible function fθ(x)≥0f_\theta(x) \ge 0 becomes a density only after dividing by ∫fθ(x) dx\int f_\theta(x)\,dx, an integral over the whole space that is intractable for neural networks. Without it, the likelihood in the maximum-likelihood objective cannot be evaluated.

Six ways around it#

Each family in the course keeps the “noise plus transformation” picture and differs in what it gives up to make training possible.

Figure

Six families: noise becomes data; what differs is what can be computed

Diagram of six families of generative modelsSix rows: VAE, GAN, flow, energy-based model, diffusion, autoregressive model. Each has a noise circle on the left, an arrow into a blue box with the learned transformation and an arrow to the sample; on the right is what the model lets us compute. Dashed elements under the rows show the helpers: the encoder of the VAE, the discriminator of the GAN, the inverse map of the flow, the unknown normalizer of the EBM, the fixed noising of diffusion and the one-value-at-a-time order of the autoregressive model.simple noiselearned transformationsamplewhat it computesVAEzdecoder pθ(x | z)xlower bound on log p(x)encoder qφ(z | x)GANzgenerator Gθ(z)xno density, samples onlydiscriminator: real?Flowzinvertible fθxexact log p(x)fθ⁻¹ backEBMx0K Langevin steps on −∇EθxKlog p(x) up to log ZZ unknown; MCMC in training tooDiffusionxTT denoising stepsx₀lower bound on log p(x)noise added by a fixed ruleAutoregr.upθ(xi | x<i), i = 1…nxexact log p(x)one value at a time, n stepsDiagram of six families of generative modelsSix blocks stacked: VAE, GAN, flow, energy-based model, diffusion, autoregressive model. Each shows a noise circle, a blue box with the learned transformation and a sample circle; below are the helper part and what the model lets us compute.VAEzdecoder pθ(x | z)xencoder qφ(z | x)computes: lower bound on log p(x)GANzgenerator Gθ(z)xdiscriminator: real?computes: no density, samples onlyFlowzinvertible fθxfθ⁻¹ backcomputes: exact log p(x)EBMx0K Langevin steps on −∇EθxKZ unknown; MCMC in training toocomputes: log p(x) up to log ZDiffusionxTT denoising stepsx₀noise added by a fixed rulecomputes: lower bound on log p(x)Autoregr.upθ(xi | x<i), i = 1…nxone value at a time, n stepscomputes: exact log p(x)
Every family produces a sample by transforming simple noise; what is learned is drawn in blue. A VAE trains a decoder together with an encoder, and training gives a lower bound on the log-likelihood (ELBO). A GAN trains a generator against a discriminator and gives no density at all. A flow uses an invertible map, so the density follows exactly from the change-of-variables formula. An energy-based model (EBM) learns only an energy E_θ(x); the density is proportional to exp(−E_θ(x)) with an unknown normalizer Z, and samples come from a noisy Markov chain: a step down the energy gradient plus a random kick, so the chain explores the distribution rather than minimizing the energy, and it may need many steps. A diffusion model learns to undo, step by step, a noising process that is fixed in advance, and also optimizes a lower bound. An autoregressive model turns uniform numbers u into values one at a time, each drawn from a learned conditional distribution given the values before it, and its likelihood is exact: the product of these conditionals.
FamilyHow a sample is drawnWhat is learnedWhat can be computedCourse notes
Variational autoencoder (VAE)z∼N(0,I)z \sim \mathcal N(\mathbf 0, \mathbf I), then decodedecoder pθ(x∣z)p_\theta(x \mid z) and encoder qϕ(z∣x)q_\phi(z \mid x)a lower bound on log⁡pθ(x)\log p_\theta(x), the ELBOVAE
Generative adversarial network (GAN)z∼p(z)z \sim p(z), then x=Gθ(z)x = G_\theta(z) in one passgenerator Gθ(z)G_\theta(z), trained against a discriminatorno density; only samplesGAN, WGAN, scaling, StyleGAN, α-GAN, CycleGAN, StarGAN, beyond CycleGAN
Normalizing flowz∼N(0,I)z \sim \mathcal N(\mathbf 0, \mathbf I), then a chain of invertible mapsinvertible fθf_\theta with a tractable Jacobianthe exact log⁡pθ(x)\log p_\theta(x)Flows, architectures
Energy-based model (EBM)start from noise, run a noisy Markov chain (Langevin) that explores e−Eθe^{-E_\theta}; many steps, mixing can be slowan energy Eθ(x)E_\theta(x), with pθ(x)=e−Eθ(x)/Zθp_\theta(x) = e^{-E_\theta(x)}/Z_\thetalog⁡pθ(x)\log p_\theta(x) up to the unknown constant log⁡Zθ\log Z_\theta; samples by MCMCMCMC samplers, EBMs
Diffusion model (DDPM)start from noise xTx_T, denoise step by step to x0x_0a network that undoes one small noising stepa lower bound on log⁡pθ(x)\log p_\theta(x)Forward process, training, score matching, latent diffusion, drag editing
Autoregressive modelone value or token at a time, each given the previous onesconditionals pθ(xi∣x<i)p_\theta(x_i \mid x_{<i}), each a softmax over values or tokensthe exact log⁡pθ(x)\log p_\theta(x)PixelRNN and PixelCNN, transformers

The last note carries these tools from images to sound: a flow that turns text into speech, and an autoregressive transformer over the tokens of a neural audio codec that generates music.

Because not every model gives a likelihood, and a likelihood does not always reflect how good samples look, the course also covers metrics computed from samples alone: Inception Score, FID, LPIPS, precision and recall and human evaluation.

The plan of the course#

  1. Foundations. Gaussians and sampling, entropy and KL divergence, fitting a distribution by minimizing KL, the training background (batch normalization, gradients, convolutions) and autoencoders.
  2. Latent-variable models. The VAE: a latent code, a lower bound on the likelihood and the reparameterization trick; β-VAE and the discrete codes of VQ-VAE.
  3. Adversarial models. GANs, the Jensen–Shannon divergence they implicitly minimize, and Wasserstein GANs; scaling GANs up (ProGAN, SAGAN, BigGAN) and controlling them (StyleGAN); autoencoders trained with adversarial losses (α-GAN); image-to-image translation without paired examples (CycleGAN), across many domains (StarGAN) and beyond (MUNIT, CUT, SPADE, super-resolution).
  4. Evaluation. IS, FID and LPIPS, precision and recall, and human evaluation.
  5. Exact likelihood. Normalizing flows and the architectures that keep the Jacobian cheap.
  6. Energy-based models. Samplers for densities known up to a constant (MALA, Hamiltonian Monte Carlo, NUTS), and energy-based models trained with contrastive divergence.
  7. Diffusion. The forward noising process and the training objective of DDPM, score matching and Langevin dynamics, latent diffusion with Stable Diffusion, and point-based editing with DragGAN and DragDiffusion.
  8. Autoregressive models. The chain rule as a model, PixelRNN and PixelCNN, and transformers over pixels and image tokens.
  9. Audio. Text-to-speech with a flow and music with an autoregressive transformer over neural-codec tokens.

What you should remember#

  • Data is a sample from an unknown pdatap_{\text{data}}; a generative model is a pθp_\theta we can sample from, fitted so that pθ≈pdatap_\theta \approx p_{\text{data}}.
  • Minimizing DKL(pdata ∥ pθ)D_{\mathrm{KL}}(p_{\text{data}} \,\|\, p_\theta) is maximum likelihood, which needs pθ(x)p_\theta(x) as a number.
  • All sampling starts from uniform numbers: inverse CDF in one dimension, Box–Muller for normals, an affine map for any Gaussian, a learned nonlinear map for everything else.
  • Every model in the course is noise plus a learned transformation; the families differ in what they can compute: a bound (VAE, diffusion), nothing (GAN), the likelihood up to a constant (energy-based models) or the exact likelihood (flows, autoregressive models).

Next#

The first step of the ladder that the course builds on is the Gaussian: how its covariance shapes the data and how an affine map turns standard noise into any Gaussian. That is Multivariate Gaussian distributions.

References#