CIS6270 / lecture_3 /README.md
pranamanam's picture
Upload 87 files
0ba9d09 verified
|
Raw History Blame Contribute Delete
12.1 kB
# Lecture 3: Score Matching and Diffusion Models
Chapter 3 of the notes builds a generator out of corruption. A stochastic differential equation
pushes a clean sample toward a standard Gaussian, Ito's lemma and the Fokker-Planck equation turn
that equation into one for the density, and the variance-preserving choice of drift and noise
scale makes the density Gaussian in closed form at every level. To reverse the motion we need the
gradient of the log density, the score, and we learn it by regression on the noise that was drawn.
Discretize the reverse equation on a grid and you get DDPM; rewrite the Laplacian as a divergence
and you get the probability-flow ODE; and one extra gradient inside either sampler gives
classifier, classifier-free and reward guidance. The last file runs the whole chain in the latent
space of a protein language model.
The clock runs from the data in every file here: `t = 0` and `k = 0` hold a clean sample and
corruption grows with `t` and with `k`, up to a standard Gaussian at the terminal level. Chapter 2
runs its clock the other way, and we keep this one throughout.
| File | Command | What it does |
| --- | --- | --- |
| `sde.py` | `python lecture_3/sde.py` | The forward process before any schedule: Brownian increments, the Euler-Maruyama step, the spreading cloud, a restoring drift, Ito's lemma on `phi(x) = x^2`, and the Fokker-Planck equation checked on `N(0, 1+t)`. |
| `forward_diffusion.py` | `python lecture_3/forward_diffusion.py` | The variance-preserving schedule and the Gaussian corruption it drives: the four noise levels located by inverting `a_t`, the thousand-step grid against the continuous curve, one pixel at step 207, the worked training batch at `abar = 0.8`, and the corruption law measured against `N(a_k x_0, sigma_k^2 I)`. |
| `score_matching.py` | `python lecture_3/score_matching.py` | Fits explicit, implicit and denoising score matching to the same one-dimensional Gaussian mixture, checks the integration by parts, locates the minimizer of each objective, and estimates a 64-dimensional trace from random directions. |
| `ddpm.py` | `python lecture_3/ddpm.py` | Trains the chapter's U-Net on 28-by-28 images with `L_simple` and samples with the K-step reverse chain. `--dataset mnist` downloads the digits. |
| `samplers.py` | `python lecture_3/samplers.py` | Ancestral DDPM, the probability-flow ODE and annealed Langevin on a four-Gaussian target whose corrupted score is exact, so nothing is trained and we compare the three on equal evaluation budgets. |
| `guidance.py` | `python lecture_3/guidance.py` | Classifier, classifier-free and reward guidance on the same target with one label per mode, with weights on the simplex and a schedule on the strength. |
| `latent_diffusion.py` | `python lecture_3/latent_diffusion.py` | AMP-Diffusion: masked corruption of residue embeddings, a transformer denoiser, reverse latent sampling, decoding through a language-model head, and a polar-composition constraint at decoding. `--dataset esm2` uses the real encoder. |
Every file takes the shared flags of `cis6270.runner.common_parser`. In `sde.py`,
`forward_diffusion.py`, `samplers.py` and `guidance.py` we work with a process we already know, so
`--steps` sets the number of simulation steps, measured levels or reverse steps; the other three
train a network, so there `--steps` sets the number of optimizer steps.
## What a default run prints
`sde.py` is deterministic arithmetic plus a Monte Carlo check. The Euler-Maruyama step from
`x = 2` lands at `2.11` with a drift displacement of `-0.04` against a noise displacement of
`0.15`, and the typical noise is `2.5` times the drift at this step size. The driftless cloud ends
with variance `0.2521` against the exact `g^2 T = 0.25`. The Ornstein-Uhlenbeck law at `t = log 2`
has mean `1` and variance `0.75`, which `16384` simulated paths reproduce as `0.9860` and `0.7458`.
Ito's lemma returns `E[W_t^2] = 0.2504` at `t = 0.25` against the exact `0.25`, and the same sum
without the curvature term returns `0.0004`. The Fokker-Planck residual on `N(0, 1+t)` is zero to
float precision, and the peak of the Gaussian falls from `0.3989` at the rate `-0.1995`, so the
one-step estimate `0.3790` sits `0.0014` below the exact `0.3804`.
`forward_diffusion.py` locates the four levels of the chapter's table at `t = 0.0589`, `0.2068`,
`0.4544` and `0.7994`, with `sigma_t` equal to `0.199`, `0.600`, `0.9367` and `0.9992` and `beta(t)`
equal to `1.2721`, `4.2157`, `9.1420` and `16.0073`; the two curves cross at `t = 0.25896`, where
both are `0.7071`. On the thousand-step grid `abar_207 = 0.6402`, so `a_207 = 0.8001` against the
continuous `0.7997` and `sigma_207 = 0.5999` against `0.6004`, and `a_1000 = 0.0064` against
`0.0066`. One pixel at `x_0 = 0.5` and `eps = -1` lands at `-0.1998`, and by step `500` the signal
is down to `0.2803`. The worked batch at `abar = 0.8` reproduces `(2.0125, -0.9839, 0.8944)`, a
loss of `0.02`, score targets `(-1.118, 0.447, -2.236)` and clean estimates `(2.05, -1.05, 0.60)`.
Corrupting one image `1024` times at each of `16` levels leaves the per-pixel sample mean within
`0.0257` of `a_k x_0` and the pooled sample variance within `0.0026` of `sigma_k^2`, and both
gaps fall as one over the square root of the number of draws.
`score_matching.py` fits three networks from the same initialization on the same batches. The
score errors against the exact field are `0.0337`, `0.1976` and `0.2073` for the explicit, implicit
and denoising objectives: the explicit fit regresses on the exact target and comes out an order of
magnitude closer, while the other two land near each other, each paying for the variance its own
estimator adds, and their ordering moves with the seed. Along the family `a s_exact` the three
minimizers come out `1.0000`, `0.9908` and `1.0053`, which is the claim that the three objectives
agree on a minimizer, measured here in the code. The integration-by-parts identity closes to about
one percent of its own size on a batch of 4096, the exact 64-dimensional trace takes 64 backward
passes and returns `-64.00` against the Hutchinson estimate `-64.67` from 32 passes, and the
posterior at `x_t = 7.4` has weights `0.2156` and `0.7844`, clean mean `9.2156`, marginal score
`-0.0765` by both routes and an irreducible residual of `0.835`.
`ddpm.py` on the offline images drops `L_simple` from `0.169` over the first hundred steps to
`0.033` over the last hundred, and the generated images land in `[-1.60, 1.50]` with a standard
deviation of `0.59`, against training data in `[-1, 1]`. The reverse posterior variance at
`k = 500` is `0.01003` and the ELBO weight it carries is `0.005508`, against `0.625` on the
two-step hand schedule. That two-step transition gives `8.6461` from both forms of the mean, a
clean estimate of `10` with interpolation weights `0.4969` and `0.4969`, variance `1/9`, and
`x_1 = 8.746`.
`samplers.py` gives all three samplers 200 score evaluations. Each finds all four modes with mode
entropy `1.3854`, `1.3848` and `1.3852` against `log 4 = 1.386`, and the mean distance to the
nearest mode is `0.4162` for ancestral DDPM, `0.4446` for the probability-flow ODE and `0.4662`
for annealed Langevin, against `0.4435` for the target itself. The mean log densities are
`-2.01`, `-2.15` and `-2.24` against the target's own `-2.15`. The worked backward step prints a
score of `-1.6`, a deterministic velocity of `0.4` reaching `1.16`, and a reverse drift of `2.0`
reaching `1.0` plus a Gaussian of standard deviation `0.4472`.
`guidance.py` sweeps the weight on the mode at `(2, 2)`. The target fraction runs `0.256`, `0.334`,
`0.506`, `0.899`, `1.000` at `w = 0, 0.1, 0.25, 0.5, 1`, and the scheduled version of `w = 1`
reaches `0.991`, slightly weaker because `zeta(t) = 4t(1-t)` switches the condition off at both
ends of the interval. Reward steering with `lambda = (1, 0)` moves the two right-hand modes from
`0.491` of the mass unguided to `0.719` at `eta = 0.5` and `0.994` at `eta = 2`, and
`lambda = (0.7, 0.3)` concentrates `0.740` on the top-right mode. The classifier route and the
classifier-free route agree on the guided noise to float precision, which is the content of the
rearrangement of Bayes' rule, and the guided backward step gives the velocity `-0.8` and the state
`1.28` from the velocity form and the same `-0.8` from the guided noise `0.2`.
`latent_diffusion.py` trains the transformer denoiser on 128 toy embeddings of 16 residues and 32
coordinates. The masked loss falls from `1.038` to `0.817`. The two numbers that bracket it are
`1.0`, which is what a denoiser that always returns zero scores on a standardized latent, and
`0.725`, the posterior variance of `Z_0` given `Z_k` under a Gaussian latent, averaged over a
uniform step index. Raising `--steps` to 2000 reaches `0.746`, close to that floor, for about
three times the running time. We decode the generated latents and get a polar fraction of
`0.3047`, and the constraint raises it to exactly `0.50` by rewriting the cheapest positions at
each sequence until eight are polar. The chapter's latent coordinate `z_0 = 1.2` corrupts to
`0.7204` at `k = 500`, a prediction of `1.05` implies `eps = 0.4438`, and the reverse mean is
`0.7193` with standard deviation `0.1002`.
## What is simplified
- `ddpm.py` uses a U-Net of base width 8, where the notes uses 64, and trains for a few hundred
steps at a learning rate of `2e-3` in place of 20 MNIST epochs at `2e-4`. Both the width and the
training budget are flags, so you can raise either one. The class, the schedule, the forward
corruption and the reverse step are the chapter's code with its names kept.
- `forward_diffusion.py` and `ddpm.py` are the two halves of the chapter's implementation section,
split so that neither file runs long. Each lecture file stands on its own, so each rebuilds the
schedule tensors; the two copies are identical, and `tests/test_lecture_3.py` asserts that they
stay so.
- `samplers.py` and `guidance.py` evaluate the exact score of the corrupted four-Gaussian mixture
in place of a trained network, so you can compare the three samplers and the guidance weights
with no training error in the way. A counter on the score stands in for the count of network
evaluations. Both read the schedule off the continuous curve, `abar_k = exp(-B(t_k))`,
in place of a linear sequence of betas, so that every `beta_k` stays below one on the coarse
grid these files run; at `K = 1000` the two agree to about `1e-3`.
- The conditional score in `guidance.py` is exact, so at `w = 1` the condition pins the mode and
the mode fractions saturate. The graded part of the sweep therefore sits below one, where a
trained conditional model would place it.
- `score_matching.py` fits a time-independent field at the single level `a_t = 0.8`,
`sigma_t = 0.6`, the chapter's running level, in place of one field across all levels.
We write the Hutchinson estimator as a vector-Jacobian product; a score field has a symmetric
Jacobian, so this equals the Jacobian-vector product of the notes at the same cost.
- `latent_diffusion.py` runs offline on `cis6270.data.toy_embeddings`, which have 32 coordinates
in place of the 1280 of ESM-2. Offline, a fixed random projection stands in for the
language-model head; the decoding path and the composition constraint run unchanged, and the
residues they read out are arbitrary. `--dataset esm2` uses the real encoder, the real head and
the real tokenizer, at `MAX_LEN = 32` in place of the chapter's 64. That path downloads a 2.5 GB
checkpoint and widens the denoiser to 1280, which is far too slow for the default `--steps` on a
laptop CPU; try 20 first. Generation there fills all `MAX_LEN` positions, including the ones
that held padding during training, exactly as the chapter's `sample_latents` does.
- The reward model of Section 3.46 is a small network on the latent. In `guidance.py` the state is
a point in the plane, so we read the two properties off its coordinates directly and keep the
same signature, which leaves nothing to train.