Change of variables in 1D
Normalizing Flows
Suppose you have a distribution that is easy to handle — say, a standard Gaussian — and you'd like to turn it into something more interesting. The machinery for doing that is the change-of-variables formula, and in one dimension the whole thing fits in a single line. This chapter is about that line and what it looks like.
The setup
Pick an invertible, differentiable function . Apply it to a sample:
The new variable has its own probability density , and we want to know what it is. The answer:
Two things to read off this. First, is determined by the base density at the preimage — to evaluate the new density at , you have to undo the transformation and ask the base density at the corresponding . Second, the answer is divided by : a stretching transformation () dilutes the density, a compressing one () concentrates it.
Why the divisor
Probability mass is conserved. If a thin slice of the input line of width contains probability , the corresponding slice of the output line has width — that's just the derivative relationship. The same probability mass now lives in a slice that is -times wider, so the output density must be smaller by the same factor:
The formula is bookkeeping — probability per unit width on the input side equals probability per unit width on the output side, accounting for the local stretching factor . There is no machine-learning anywhere in this derivation; it's the same identity every undergraduate calculus course teaches under "u-substitution," read as a statement about densities.
The visualization
Below: a standard Gaussian on top, the same samples pushed through on the bottom. Each colored dot above is a sample; the matching dot below is where it lands after the transform. The shaded curves are the density before and after. Drag the slider — or click a preset — to see how the output density changes with the nonlinearity parameter . The factor of 2 inside the sine puts the action right inside the Gaussian's main mass, so the effect is large enough to be visible.
y = x + α · sin(2x). Each colored dot above is a sample x ~ N(0, 1); its matching dot below is y = f(x).
A walk through what each preset is doing.
Identity (). The transform is . The output density equals the input — no stretching anywhere, derivative is 1 everywhere, formula gives . The dots stay where they started. This is the trivial flow; everything else builds on it.
Mild stretch (). The derivative is largest near () and smallest near (). So the central x's get stretched apart and the x's near get squeezed together. In density-land that means the centre flattens and small humps start to grow at (the images of the squeezed regions). At this slider position the humps are subtle — keep going.
Bimodal (). Same direction, near the limit. : the transform compresses by a factor of 10 right where the base Gaussian still has decent mass (). The change-of-variables formula divides by , giving output density peaks of at — the bottom panel auto-rescales to fit them, and you can see the central Gaussian peak (still around 0.4 / 1.5 ≈ 0.27 in absolute terms) become small in comparison. The result is a bimodal output density built from a unimodal Gaussian by a single, smooth, invertible function. This is the central thing a flow does.
Compression (). Sign flip. Now the centre compresses () while the regions near stretch. The central peak of the output density gets taller, and the wings flatten. The dots in the middle of the input bunch up in the middle of the output; the dots away from centre spread out.
Sharp peak (). Near the limit in the other direction. The central derivative is , so the output density at reaches . The bottom panel rescales to fit the spike, and you can see how thoroughly the wide input has been compressed into a single tall peak around zero — most of the input mass has been crammed into a very narrow window of .
The whole story is in the slider: the shape of the output density is dictated by where the transform stretches and where it compresses. A flow is just an explicit choice of those stretches.
The log version
For training, we work with log-densities — products turn into sums and there's no risk of underflow. Take logs of the change-of-variables formula:
Two terms. The first is the base log-density at the preimage (cheap — for a Gaussian it's a quadratic in ). The second is the log of the absolute derivative of the transform, sometimes called the log-determinant of the Jacobian — in 1D the "Jacobian" is just . Every flow architecture you'll meet later is in some sense an answer to one question: how do we make that second term cheap to compute when the input lives in high-dimensional space?
For now the answer is "it's a single derivative, so it's free," and that's enough to make the rest of the series work. The next chapter composes 1D flows: stacking transforms gives a much richer family of output densities, and the log-density formula picks up exactly one extra term per layer.
Why does the formula need to be invertible?
Because the inverse appears explicitly on the right-hand side. A non-invertible would have multiple 's mapping to the same , and the density at would have to sum the contributions from all of them. That's the formula for a non-invertible push-forward, and it's strictly more expensive (you have to enumerate all preimages). For flows, requiring invertibility is the price we pay for keeping the formula clean and tractable. It also means we can generate by sampling and applying , AND we can evaluate the density of any by applying and the formula. One model, two capabilities.
What if the transform is not a bijection — say, ?
Then it doesn't qualify as a flow. The push-forward density still exists ( has a chi-squared distribution if is standard normal), but computing it requires the multi-preimage formula and the transform isn't invertible — you can sample from but you can't go back. The whole point of a normalizing flow is that you can go back: the forward direction generates samples, the inverse direction evaluates densities, and both directions are exact. Drop invertibility and you've just got a generative model; it's not a flow anymore.
Why call it "normalizing"?
Because the inverse direction normalizes data into the base distribution. Given a complicated empirical sample of values, applying should yield values that look like draws from the base — a standard Gaussian. That's where "normalizing" comes from: the flow is the change of variables that turns data into a standard normal. We then read the term "normalizing flow" forwards, but the underlying meaning points the other way.