an autoencoder is a network trained to do the one thing that sounds useless: output its own input. the trick is the obstacle course in the middle — a bottleneck, a corruption, a penalty — that makes verbatim copying impossible, so the network is forced to learn what about the input is worth keeping. 𐃏 the family tree below runs from the linear special case (which is pca wearing a trenchcoat) to the variational autoencoder, which turns the whole construction into a generative model (Goodfellow, Ian, 2016).
Stable Diffusion Models
diffusion models generate by learning to undo noise: destroy an image with a fixed gaussian corruption process, train a network to reverse one small step of the destruction, then chain the reversals from pure noise back to data (ho et al. 2020, denoising diffusion probabilistic models). 𐃏 stable diffusion (rombach et al. 2022, high-resolution image synthesis with latent diffusion models) runs this machinery not on pixels but in the latent space of an autoencoder, with a text-conditioned u-net doing the denoising. first the maths, then the architecture.
the forward (noising) process
fix a variance schedule \(\beta_1, \dots, \beta_T \in (0,1)\) and define a markov chain that gradually gaussianises the data:
\begin{equation} q(x_t \mid x_{t-1}) = \mathcal{N}\!\left(x_t;\; \sqrt{1 - \beta_t}\, x_{t-1},\; \beta_t I\right). \end{equation}
the \(\sqrt{1-\beta_t}\) shrinkage is chosen so variance is preserved: if \(\operatorname{Var}(x_{t-1}) = I\) then \(\operatorname{Var}(x_t) = (1-\beta_t) I + \beta_t I = I\). because compositions of these gaussians are gaussian, the chain telescopes into a closed form — with \(\alpha_t = 1 - \beta_t\) and \(\bar{\alpha}_t = \prod_{s=1}^{t} \alpha_s\),
\begin{equation} q(x_t \mid x_0) = \mathcal{N}\!\left(x_t;\; \sqrt{\bar{\alpha}_t}\, x_0,\; (1 - \bar{\alpha}_t) I\right) \quad\Longleftrightarrow\quad x_t = \sqrt{\bar{\alpha}_t}\, x_0 + \sqrt{1 - \bar{\alpha}_t}\, \varepsilon,\;\; \varepsilon \sim \mathcal{N}(0, I). \end{equation}
this is the engine of cheap training: any noise level \(t\) is reachable from \(x_0\) in one step, no simulation of the chain required. as \(t \to T\) (with \(\bar{\alpha}_T \approx 0\)), \(x_T\) is indistinguishable from \(\mathcal{N}(0, I)\) — the data has been fully forgotten.
from variational bound to epsilon-prediction
the model is a latent-variable model \(p_\theta(x_0) = \int p(x_T) \prod_{t=1}^{T} p_\theta(x_{t-1} \mid x_t)\, dx_{1:T}\) with gaussian reverse kernels, trained by the usual variational bound on negative log-likelihood (Goodfellow, Ian, 2016):
\begin{equation} \mathbb{E}\big[-\log p_\theta(x_0)\big] \le \mathbb{E}_q\!\left[ \underbrace{\mathrm{KL}\big(q(x_T \mid x_0) \,\|\, p(x_T)\big)}_{L_T \approx 0} + \sum_{t>1} \underbrace{\mathrm{KL}\big(q(x_{t-1} \mid x_t, x_0) \,\|\, p_\theta(x_{t-1} \mid x_t)\big)}_{L_{t-1}} - \underbrace{\log p_\theta(x_0 \mid x_1)}_{L_0} \right]. \end{equation}
the pivotal fact: the forward posterior \(q(x_{t-1} \mid x_t, x_0)\) is itself gaussian with known mean
\begin{equation} \tilde{\mu}_t(x_t, x_0) = \frac{\sqrt{\bar{\alpha}_{t-1}}\, \beta_t}{1 - \bar{\alpha}_t}\, x_0 + \frac{\sqrt{\alpha_t}\, (1 - \bar{\alpha}_{t-1})}{1 - \bar{\alpha}_t}\, x_t, \end{equation}
so each \(L_{t-1}\) is a kl between gaussians — a weighted squared distance between means. now reparameterise: substitute \(x_0 = \big(x_t - \sqrt{1-\bar\alpha_t}\,\varepsilon\big)/\sqrt{\bar\alpha_t}\) and let the network predict the noise \(\varepsilon_\theta(x_t, t)\) rather than the mean. the bound becomes a weighted sum of terms \(\lVert \varepsilon - \varepsilon_\theta(x_t, t) \rVert^2\). the ddpm result, stated honestly: ho et al. drop the weights and train on the uniform-weight surrogate
\begin{equation} L_{\text{simple}} = \mathbb{E}_{t \sim \mathcal{U}\{1..T\},\, x_0,\, \varepsilon} \left[ \big\lVert \varepsilon - \varepsilon_\theta\big(\sqrt{\bar\alpha_t}\, x_0 + \sqrt{1-\bar\alpha_t}\,\varepsilon,\; t\big) \big\rVert^2 \right], \end{equation}
which is not the elbo but a reweighting of it that downweights the easy low-\(t\) terms — and produces better samples than the exact bound. the entire training loop is: pick an image, pick a timestep, add closed-form noise, regress the noise. no adversary, no equilibrium, compare gans.
the reverse process and sampling
the learned reverse kernel is \(p_\theta(x_{t-1} \mid x_t) = \mathcal{N}\big(x_{t-1};\, \mu_\theta(x_t, t),\, \sigma_t^2 I\big)\) with the mean expressed through the predicted noise:
\begin{equation} \mu_\theta(x_t, t) = \frac{1}{\sqrt{\alpha_t}} \left( x_t - \frac{\beta_t}{\sqrt{1 - \bar{\alpha}_t}}\, \varepsilon_\theta(x_t, t) \right), \qquad \sigma_t^2 = \beta_t \;\text{(or the posterior variance; both work)}. \end{equation}
ancestral sampling is then a loop:
- init \(x_T \sim \mathcal{N}(0, I)\).
- loop for \(t = T, \dots, 1\): predict \(\varepsilon_\theta(x_t, t)\), form \(\mu_\theta\), set \(x_{t-1} = \mu_\theta + \sigma_t z\) with \(z \sim \mathcal{N}(0,I)\) (no noise at \(t=1\)).
- return \(x_0\).
that is \(T\) network evaluations per sample — ddpm used \(T = 1000\), which is the method’s honest weakness. ddim (song et al. 2020, denoising diffusion implicit models) reinterprets the same trained \(\varepsilon_\theta\) inside a non-markovian family whose deterministic member permits taking large strides over the schedule — 20–50 steps for near-identical quality, and, being deterministic, an invertible map between noise and image. 𐃏
latent diffusion: the stable part
pixel-space diffusion at 512px spends most of its capacity on imperceptible high-frequency detail. latent diffusion moves the chain into a compressed space:
- vae latent space. a perceptually-trained autoencoder (kl-regularised, with a patch-adversarial loss on the decoder) maps a \(512 \times 512 \times 3\) image to a \(64 \times 64 \times 4\) latent — spatial factor 8, ~48x fewer numbers. diffusion happens entirely on these latents; see autoencoders.
- u-net denoiser. \(\varepsilon_\theta\) is a u-net — downsampling and upsampling convolutional stages with skip connections — with the timestep injected as an embedding into every residual block.
- cross-attention conditioning. the prompt goes through a frozen clip text encoder into a sequence of embeddings; at several u-net resolutions, attention layers use image features as queries and text embeddings as keys and values, so every spatial location can consult every word.
- decode. after the reverse loop finishes, the vae decoder maps the final latent back to pixels — one pass, at the very end.
classifier-free guidance
conditioning alone produces washed-out adherence to the prompt. cfg (ho and salimans 2022, classifier-free diffusion guidance) trains one network for both conditional and unconditional prediction by randomly dropping the conditioning during training, then at sampling extrapolates past the conditional prediction:
\begin{equation} \tilde{\varepsilon}_\theta(z_t, c) = \varepsilon_\theta(z_t, \varnothing) + w \big( \varepsilon_\theta(z_t, c) - \varepsilon_\theta(z_t, \varnothing) \big), \end{equation}
with guidance scale \(w > 1\) (stable diffusion defaults to \(w \approx 7.5\)). the difference vector points from “any image” toward “images matching \(c\)”; scaling it trades diversity for prompt fidelity, and overdoing it gives the over-saturated, over-contrasty look every heavy user recognises. cost: two u-net evaluations per step.
toy diffusion in 1d
the full pipeline — closed-form forward noising, an mlp \(\varepsilon\)-predictor with hand-written backprop, ancestral sampling — on the most brutal test of mode coverage: a dataset of exactly two points, \(x_0 \in \{-1, +1\}\). a mode-collapsing model would pick one.
import numpy as np
rng = np.random.default_rng(0)
# --- schedule: T steps, linear betas, closed-form alpha-bars
T = 50
betas = np.linspace(1e-4, 0.25, T)
alphas = 1 - betas
abar = np.cumprod(alphas) # \bar\alpha_t
# --- data: two points, x0 = -1 or +1 with equal probability
sample_data = lambda n: rng.choice([-1.0, 1.0], size=(n, 1))
# --- epsilon-predictor: tiny mlp, input (x_t, t/T), output eps_hat
p = {"W1": rng.normal(0, 0.5, (2, 32)), "b1": np.zeros(32),
"W2": rng.normal(0, 0.5, (32, 1)), "b2": np.zeros(1)}
m = {k: 0.0 for k in p}; v = {k: 0.0 for k in p}
def forward(inp):
h = np.tanh(inp @ p["W1"] + p["b1"])
return h, h @ p["W2"] + p["b2"]
def adam_step(g, t, lr=2e-3):
for k in p:
m[k] = 0.9*m[k] + 0.1*g[k]
v[k] = 0.999*v[k] + 0.001*g[k]**2
p[k] -= lr * (m[k]/(1-0.9**t)) / (np.sqrt(v[k]/(1-0.999**t)) + 1e-8)
# --- training: sample t, noise with the closed form, regress the noise
B = 256
for step in range(1, 8001):
x0 = sample_data(B)
t = rng.integers(0, T, B)
eps = rng.normal(size=(B, 1))
xt = np.sqrt(abar[t])[:, None]*x0 + np.sqrt(1 - abar[t])[:, None]*eps
inp = np.hstack([xt, (t[:, None] + 1)/T])
h, eps_hat = forward(inp)
d = 2*(eps_hat - eps)/B # d(mse)/d(eps_hat)
g = {"W2": h.T @ d, "b2": d.sum(0)}
dh = (d @ p["W2"].T) * (1 - h**2)
g["W1"], g["b1"] = inp.T @ dh, dh.sum(0)
adam_step(g, step)
if step in (1, 1000, 4000, 8000):
print(f"step {step:>5} mse {((eps_hat - eps)**2).mean():.4f}")
# --- ancestral sampling: run the learned reverse chain from pure noise
n = 4000
x = rng.normal(size=(n, 1)) # x_T ~ N(0, 1)
for t in reversed(range(T)):
_, eps_hat = forward(np.hstack([x, np.full((n, 1), (t + 1)/T)]))
x = (x - betas[t]/np.sqrt(1 - abar[t]) * eps_hat) / np.sqrt(alphas[t])
if t > 0:
x += np.sqrt(betas[t]) * rng.normal(size=(n, 1))
near_pos = (np.abs(x - 1) < 0.3).mean()
near_neg = (np.abs(x + 1) < 0.3).mean()
print(f"\nsamples: mean {x.mean():+.3f}, std {x.std():.3f}")
print(f"fraction within 0.3 of +1: {near_pos:.3f}, of -1: {near_neg:.3f}")
print("mean of |x| (target 1.0):", f"{np.abs(x).mean():.3f}")
step 1 mse 2.8974
step 1000 mse 0.3682
step 4000 mse 0.2710
step 8000 mse 0.1701
samples: mean +0.051, std 1.005
fraction within 0.3 of +1: 0.473, of -1: 0.435
mean of |x| (target 1.0): 0.989
starting from a single gaussian blob, the reverse chain splits the mass onto both modes — 47% lands on \(+1\), 44% on \(-1\), mean of \(|x|\) is \(0.989\) against a target of \(1.0\). the residual mse never reaches zero and should not: at high \(t\) the noise is genuinely unrecoverable, so a fraction of \(\lVert\varepsilon\rVert^2\) is irreducible.
see also
- gans — the adversarial predecessor, and what mode collapse looks like
- autoencoders — the compression stage that makes latent diffusion cheap
- transformers — the cross-attention mechanism doing the text conditioning
- cnn from scratch — the convolutional anatomy of the u-net
References
Goodfellow, Ian (2016). Deep Learning, MIT Press.
Backlinks (5)
1. Autoencoders
2. GAN: Generative Adversarial Networks /wiki/ml/dl/gans/
a gan trains a generator by making it play a game against a learned critic: the generator \(G\) maps noise to samples, the discriminator \(D\) tries to tell those samples from real data, and each improves by exploiting the other’s current weakness — density estimation recast as a two-player minimax game (goodfellow et al. 2014, generative adversarial networks). 𐃏 the framework is treated in ch. 20 of (Goodfellow, Ian, 2016).
3. Visual Transformers (ViT) /wiki/ml/dl/computer-vision/visual-transformers/
the vision transformer asks a blunt question: if attention replaced recurrence for text, can it replace convolution for images? the answer (dosovitskiy et al. 2020, an image is worth 16x16 words) is yes — chop the image into patches, treat each patch as a token, and feed the sequence to a standard transformer encoder with almost no vision-specific machinery. 𐃏
4. Wiki /wiki/
Knowledge is a paradox. The more one understand, the more one realises the vastness of his ignorance.