Delete lecture_3
Browse files- lecture_3/README.md +0 -139
- lecture_3/ddpm.py +0 -315
- lecture_3/forward_diffusion.py +0 -204
- lecture_3/guidance.py +0 -280
- lecture_3/latent_diffusion.py +0 -309
- lecture_3/samplers.py +0 -247
- lecture_3/score_matching.py +0 -302
- lecture_3/sde.py +0 -249
lecture_3/README.md
DELETED
|
@@ -1,139 +0,0 @@
|
|
| 1 |
-
# Lecture 3: Score Matching and Diffusion Models
|
| 2 |
-
|
| 3 |
-
Chapter 3 of the notes builds a generator out of corruption. A clean sample is pushed toward a
|
| 4 |
-
standard Gaussian by a stochastic differential equation, Ito's lemma and the Fokker-Planck
|
| 5 |
-
equation turn that equation into one for the density, and the variance-preserving choice of drift
|
| 6 |
-
and noise scale makes the density Gaussian in closed form at every level. Reversing the motion
|
| 7 |
-
needs the gradient of the log density, the score, which is learned by regression on the noise that
|
| 8 |
-
was drawn. Discretizing the reverse equation on a grid gives DDPM; rewriting the Laplacian as a
|
| 9 |
-
divergence gives the probability-flow ODE; and one extra gradient inside either sampler gives
|
| 10 |
-
classifier, classifier-free and reward guidance. The last file runs the whole chain in the latent
|
| 11 |
-
space of a protein language model.
|
| 12 |
-
|
| 13 |
-
The clock runs from the data in every file here: `t = 0` and `k = 0` hold a clean sample and
|
| 14 |
-
corruption grows with `t` and with `k`, up to a standard Gaussian at the terminal level. This is
|
| 15 |
-
the reverse of Chapter 2's convention and it is kept throughout.
|
| 16 |
-
|
| 17 |
-
| File | Command | What it does |
|
| 18 |
-
| --- | --- | --- |
|
| 19 |
-
| `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)`. |
|
| 20 |
-
| `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)`. |
|
| 21 |
-
| `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. |
|
| 22 |
-
| `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. |
|
| 23 |
-
| `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 the three are compared on equal evaluation budgets. |
|
| 24 |
-
| `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. |
|
| 25 |
-
| `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. |
|
| 26 |
-
|
| 27 |
-
Every file takes the shared flags of `cis6270.runner.common_parser`. In `sde.py`,
|
| 28 |
-
`forward_diffusion.py`, `samplers.py` and `guidance.py` nothing is trained, so `--steps` sets the
|
| 29 |
-
number of simulation steps, measured levels or reverse steps; in the other three it sets the
|
| 30 |
-
number of optimizer steps.
|
| 31 |
-
|
| 32 |
-
## What a default run prints
|
| 33 |
-
|
| 34 |
-
`sde.py` is deterministic arithmetic plus a Monte Carlo check. The Euler-Maruyama step from
|
| 35 |
-
`x = 2` lands at `2.11` with a drift displacement of `-0.04` against a noise displacement of
|
| 36 |
-
`0.15`, and the typical noise is `2.5` times the drift at this step size. The driftless cloud ends
|
| 37 |
-
with variance `0.2521` against the exact `g^2 T = 0.25`. The Ornstein-Uhlenbeck law at `t = log 2`
|
| 38 |
-
has mean `1` and variance `0.75`, which `16384` simulated paths reproduce as `0.9860` and `0.7458`.
|
| 39 |
-
Ito's lemma returns `E[W_t^2] = 0.2504` at `t = 0.25` against the exact `0.25`, and the same sum
|
| 40 |
-
without the curvature term returns `0.0004`. The Fokker-Planck residual on `N(0, 1+t)` is zero to
|
| 41 |
-
float precision, and the peak of the Gaussian falls from `0.3989` at the rate `-0.1995`, so the
|
| 42 |
-
one-step estimate `0.3790` sits `0.0014` below the exact `0.3804`.
|
| 43 |
-
|
| 44 |
-
`forward_diffusion.py` locates the four levels of the chapter's table at `t = 0.0589`, `0.2068`,
|
| 45 |
-
`0.4544` and `0.7994`, with `sigma_t` equal to `0.199`, `0.600`, `0.9367` and `0.9992` and `beta(t)`
|
| 46 |
-
equal to `1.2721`, `4.2157`, `9.1420` and `16.0073`; the two curves cross at `t = 0.25896`, where
|
| 47 |
-
both are `0.7071`. On the thousand-step grid `abar_207 = 0.6402`, so `a_207 = 0.8001` against the
|
| 48 |
-
continuous `0.7997` and `sigma_207 = 0.5999` against `0.6004`, and `a_1000 = 0.0064` against
|
| 49 |
-
`0.0066`. One pixel at `x_0 = 0.5` and `eps = -1` lands at `-0.1998`, and by step `500` the signal
|
| 50 |
-
is down to `0.2803`. The worked batch at `abar = 0.8` reproduces `(2.0125, -0.9839, 0.8944)`, a
|
| 51 |
-
loss of `0.02`, score targets `(-1.118, 0.447, -2.236)` and clean estimates `(2.05, -1.05, 0.60)`.
|
| 52 |
-
Corrupting one image `1024` times at each of `16` levels leaves the per-pixel sample mean within
|
| 53 |
-
`0.0257` of `a_k x_0` and the pooled sample variance within `0.0026` of `sigma_k^2`, and both
|
| 54 |
-
gaps fall as one over the square root of the number of draws.
|
| 55 |
-
|
| 56 |
-
`score_matching.py` fits three networks from the same initialization on the same batches. The
|
| 57 |
-
score errors against the exact field are `0.0337`, `0.1976` and `0.2073` for the explicit, implicit
|
| 58 |
-
and denoising objectives: the explicit fit regresses on the exact target and comes out an order of
|
| 59 |
-
magnitude closer, while the other two land near each other, each paying for the variance its own
|
| 60 |
-
estimator adds. The ordering of those two moves with the seed. Along the family `a s_exact` the
|
| 61 |
-
three minimizers come out `1.0000`, `0.9908` and `1.0053`, which is the claim that the three
|
| 62 |
-
objectives agree on a minimizer, measured and not merely argued. The integration-by-parts identity
|
| 63 |
-
closes to about one percent of its own size on a batch of 4096, the exact 64-dimensional trace
|
| 64 |
-
takes 64 backward passes and returns `-64.00` against the Hutchinson estimate `-64.67` from 32
|
| 65 |
-
passes, and the posterior at `x_t = 7.4` has weights `0.2156` and `0.7844`, clean mean `9.2156`,
|
| 66 |
-
marginal score `-0.0765` by both routes and an irreducible residual of `0.835`.
|
| 67 |
-
|
| 68 |
-
`ddpm.py` on the offline images drops `L_simple` from `0.169` over the first hundred steps to
|
| 69 |
-
`0.033` over the last hundred, and the generated images land in `[-1.60, 1.50]` with a standard
|
| 70 |
-
deviation of `0.59`, against training data in `[-1, 1]`. The reverse posterior variance at
|
| 71 |
-
`k = 500` is `0.01003` and the ELBO weight it carries is `0.005508`, against `0.625` on the
|
| 72 |
-
two-step hand schedule. That two-step transition gives `8.6461` from both forms of the mean, a
|
| 73 |
-
clean estimate of `10` with interpolation weights `0.4969` and `0.4969`, variance `1/9`, and
|
| 74 |
-
`x_1 = 8.746`.
|
| 75 |
-
|
| 76 |
-
`samplers.py` gives all three samplers 200 score evaluations. Each finds all four modes with mode
|
| 77 |
-
entropy `1.3854`, `1.3848` and `1.3852` against `log 4 = 1.386`, and the mean distance to the
|
| 78 |
-
nearest mode is `0.4162` for ancestral DDPM, `0.4446` for the probability-flow ODE and `0.4662`
|
| 79 |
-
for annealed Langevin, against `0.4435` for the target itself. The mean log densities are
|
| 80 |
-
`-2.01`, `-2.15` and `-2.24` against the target's own `-2.15`. The worked backward step prints a
|
| 81 |
-
score of `-1.6`, a deterministic velocity of `0.4` reaching `1.16`, and a reverse drift of `2.0`
|
| 82 |
-
reaching `1.0` plus a Gaussian of standard deviation `0.4472`.
|
| 83 |
-
|
| 84 |
-
`guidance.py` sweeps the weight on the mode at `(2, 2)`. The target fraction runs `0.256`, `0.334`,
|
| 85 |
-
`0.506`, `0.899`, `1.000` at `w = 0, 0.1, 0.25, 0.5, 1`, and the scheduled version of `w = 1`
|
| 86 |
-
reaches `0.991`, slightly weaker because `zeta(t) = 4t(1-t)` switches the condition off at both
|
| 87 |
-
ends of the interval. Reward steering with `lambda = (1, 0)` moves the two right-hand modes from
|
| 88 |
-
`0.491` of the mass unguided to `0.719` at `eta = 0.5` and `0.994` at `eta = 2`, and
|
| 89 |
-
`lambda = (0.7, 0.3)` concentrates `0.740` on the top-right mode. The classifier route and the
|
| 90 |
-
classifier-free route agree on the guided noise to float precision, which is the content of the
|
| 91 |
-
rearrangement of Bayes' rule, and the guided backward step gives the velocity `-0.8` and the state
|
| 92 |
-
`1.28` from the velocity form and the same `-0.8` from the guided noise `0.2`.
|
| 93 |
-
|
| 94 |
-
`latent_diffusion.py` trains the transformer denoiser on 128 toy embeddings of 16 residues and 32
|
| 95 |
-
coordinates. The masked loss falls from `1.038` to `0.817`. The two numbers that bracket it are
|
| 96 |
-
`1.0`, which is what a denoiser that always returns zero scores on a standardized latent, and
|
| 97 |
-
`0.725`, the posterior variance of `Z_0` given `Z_k` under a Gaussian latent, averaged over a
|
| 98 |
-
uniform step index. Raising `--steps` to 2000 reaches `0.746`, close to that floor, for about
|
| 99 |
-
three times the running time. Decoding the generated latents gives a polar fraction of `0.3047`, and the constraint
|
| 100 |
-
raises it to exactly `0.50` by rewriting the cheapest positions at each sequence until eight are
|
| 101 |
-
polar. The chapter's latent coordinate `z_0 = 1.2` corrupts to `0.7204` at `k = 500`, a prediction
|
| 102 |
-
of `1.05` implies `eps = 0.4438`, and the reverse mean is `0.7193` with standard deviation
|
| 103 |
-
`0.1002`.
|
| 104 |
-
|
| 105 |
-
## What is simplified
|
| 106 |
-
|
| 107 |
-
- `ddpm.py` uses a U-Net of base width 8, where the notes uses 64, and trains for a few hundred
|
| 108 |
-
steps at a learning rate of `2e-3` in place of 20 MNIST epochs at `2e-4`. Both are flags.
|
| 109 |
-
The class, the schedule, the forward corruption and the reverse step are the chapter's code with
|
| 110 |
-
its names kept.
|
| 111 |
-
- `forward_diffusion.py` and `ddpm.py` are the two halves of the chapter's implementation section,
|
| 112 |
-
split so that neither file runs long. No lecture file imports another, so each rebuilds the
|
| 113 |
-
schedule tensors; the two copies are identical, and `tests/test_lecture_3.py` asserts that they
|
| 114 |
-
stay so.
|
| 115 |
-
- `samplers.py` and `guidance.py` evaluate the exact score of the corrupted four-Gaussian mixture
|
| 116 |
-
in place of a trained network, which is what lets the three samplers and the guidance weights be
|
| 117 |
-
compared with no training error in the way. A counter on the score stands in for the count of
|
| 118 |
-
network evaluations. Both read the schedule off the continuous curve, `abar_k = exp(-B(t_k))`,
|
| 119 |
-
and not from a linear sequence of betas, so that every `beta_k` stays below one on the coarse
|
| 120 |
-
grid these files run; at `K = 1000` the two agree to about `1e-3`.
|
| 121 |
-
- The conditional score in `guidance.py` is exact, so at `w = 1` the condition pins the mode and
|
| 122 |
-
the mode fractions saturate. The graded part of the sweep therefore sits below one, where a
|
| 123 |
-
trained conditional model would place it.
|
| 124 |
-
- `score_matching.py` fits a time-independent field at the single level `a_t = 0.8`,
|
| 125 |
-
`sigma_t = 0.6`, the chapter's running level, in place of one field across all levels.
|
| 126 |
-
The Hutchinson estimator is written as a vector-Jacobian product; a score field has a symmetric
|
| 127 |
-
Jacobian, so this equals the Jacobian-vector product of the notes at the same cost.
|
| 128 |
-
- `latent_diffusion.py` runs offline on `cis6270.data.toy_embeddings`, which have 32 coordinates
|
| 129 |
-
in place of the 1280 of ESM-2. Without the encoder there is no language-model head, so a fixed
|
| 130 |
-
random projection stands in for it; the decoding path and the composition constraint run
|
| 131 |
-
unchanged, and the residues they read out carry no biology. `--dataset esm2` uses the real
|
| 132 |
-
encoder, the real head and the real tokenizer, at `MAX_LEN = 32` in place of the chapter's 64.
|
| 133 |
-
That path downloads a 2.5 GB checkpoint and widens the denoiser to 1280, which is far too slow
|
| 134 |
-
for the default `--steps` on a laptop CPU; try 20 first. Generation there fills all `MAX_LEN`
|
| 135 |
-
positions, including the ones that held padding during training, exactly as the chapter's
|
| 136 |
-
`sample_latents` does.
|
| 137 |
-
- The reward model of Section 3.46 is a small network on the latent. In `guidance.py` the state is
|
| 138 |
-
a point in the plane, so the two properties are read off its coordinates directly and keep the
|
| 139 |
-
same signature, which leaves nothing to train.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_3/ddpm.py
DELETED
|
@@ -1,315 +0,0 @@
|
|
| 1 |
-
"""DDPM, the reverse-time equation written on a finite grid.
|
| 2 |
-
|
| 3 |
-
A U-Net reads a corrupted image and the step it sits at, and regresses the Gaussian draw that
|
| 4 |
-
corrupted it. The reverse transition rebuilds the previous state from that one prediction, with a
|
| 5 |
-
mean that comes from completing the square and the posterior variance as its spread, and the
|
| 6 |
-
sampler starts at a standard Gaussian and applies that transition K times. The three pieces match
|
| 7 |
-
the derivation one to one, with the chapter's names kept. The schedule and the forward corruption
|
| 8 |
-
are the subject of `forward_diffusion.py`, rebuilt here so that this file stands alone.
|
| 9 |
-
|
| 10 |
-
Chapter 3 runs its diffusion clock from the data, so step k = 0 holds a clean image and corruption
|
| 11 |
-
grows with k, up to pure noise at k = K.
|
| 12 |
-
|
| 13 |
-
Chapter 3, Sections 3.19, 3.20 and 3.24 through 3.28 of the notes. Trained on 28-by-28 images, the
|
| 14 |
-
offline toy set by default and MNIST with --dataset mnist. Prints L_simple over the run, the
|
| 15 |
-
reverse posterior variance and the ELBO weight it carries, the chapter's two-step reverse
|
| 16 |
-
transition, and the range of the generated images.
|
| 17 |
-
|
| 18 |
-
python lecture_3/ddpm.py # 300 steps on the offline images
|
| 19 |
-
python lecture_3/ddpm.py --steps 20 # a few seconds, for a first look
|
| 20 |
-
python lecture_3/ddpm.py --dataset mnist # the real digits, downloaded on first use
|
| 21 |
-
"""
|
| 22 |
-
|
| 23 |
-
# %% 01. Imports and configuration
|
| 24 |
-
import math
|
| 25 |
-
|
| 26 |
-
import torch
|
| 27 |
-
import torch.nn as nn
|
| 28 |
-
import torch.nn.functional as F
|
| 29 |
-
|
| 30 |
-
from cis6270.nets import count_parameters
|
| 31 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed, train
|
| 32 |
-
|
| 33 |
-
NUM_STEPS = 1000 # Diffusion steps K.
|
| 34 |
-
BETA_START = 1e-4 # Noise variance added at step 1.
|
| 35 |
-
BETA_END = 0.02 # Noise variance added at step K.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
# %% 02. The schedule tensors and the forward corruption
|
| 39 |
-
#
|
| 40 |
-
# alpha_k = 1 - beta_k is the one-step retention, abar_k its running product, and beta-tilde_k the
|
| 41 |
-
# variance of the reverse posterior q(x_{k-1} | x_k, x_0). A zero is prefixed to the betas so that
|
| 42 |
-
# Python index k holds beta_k and the cumulative product starts at abar_0 = 1, the clean state.
|
| 43 |
-
# One Gaussian draw then reaches any level, x_k = a_k x_0 + sigma_k eps with a_k = sqrt(abar_k) and
|
| 44 |
-
# sigma_k = sqrt(1 - abar_k).
|
| 45 |
-
|
| 46 |
-
betas = torch.cat([
|
| 47 |
-
torch.zeros(1), # Placeholder so that index k holds beta_k.
|
| 48 |
-
torch.linspace(BETA_START, BETA_END, NUM_STEPS), # beta_1 ... beta_K.
|
| 49 |
-
])
|
| 50 |
-
alphas = 1.0 - betas # One-step signal retention alpha_k.
|
| 51 |
-
alphas_cumprod = torch.cumprod(alphas, dim=0) # Cumulative retention.
|
| 52 |
-
alphas_cumprod_prev = torch.cat([
|
| 53 |
-
torch.ones(1), # abar_0 = 1.
|
| 54 |
-
alphas_cumprod[:-1], # Index k now holds abar_{k-1}.
|
| 55 |
-
])
|
| 56 |
-
posterior_variance = (
|
| 57 |
-
betas * (1.0 - alphas_cumprod_prev) / (1.0 - alphas_cumprod)
|
| 58 |
-
) # Reverse variance beta-tilde_k.
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def extract(schedule, k, shape):
|
| 62 |
-
# One schedule value per item, reshaped to broadcast over C, H, W.
|
| 63 |
-
out = schedule.to(k.device).gather(0, k)
|
| 64 |
-
return out.reshape(k.shape[0], *((1,) * (len(shape) - 1)))
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def q_sample(x_0, k, noise=None):
|
| 68 |
-
if noise is None:
|
| 69 |
-
noise = torch.randn_like(x_0) # Mean 0, covariance I, shape of x_0.
|
| 70 |
-
abar = extract(alphas_cumprod, k, x_0.shape)
|
| 71 |
-
a_k = abar.sqrt() # Signal coefficient a_k.
|
| 72 |
-
sigma_k = (1.0 - abar).sqrt() # Noise standard deviation sigma_k.
|
| 73 |
-
return a_k * x_0 + sigma_k * noise, noise # x_k and the exact noise.
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
# %% 03. Sinusoidal step embedding
|
| 77 |
-
#
|
| 78 |
-
# A raw index is a poor input to a linear layer, so the step is spread over many frequencies first.
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def timestep_embedding(k, dim):
|
| 82 |
-
half = dim // 2 # Half the coordinates sine, half cosine.
|
| 83 |
-
freqs = torch.exp(
|
| 84 |
-
-math.log(10000.0)
|
| 85 |
-
* torch.arange(half, device=k.device).to(torch.get_default_dtype()) / half
|
| 86 |
-
)
|
| 87 |
-
args = k.to(torch.get_default_dtype())[:, None] * freqs[None, :] # [B, half].
|
| 88 |
-
return torch.cat([torch.sin(args), torch.cos(args)], dim=1) # [B, dim].
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def num_groups(channels):
|
| 92 |
-
for g in (8, 4, 2, 1): # Largest valid GroupNorm group count.
|
| 93 |
-
if channels % g == 0:
|
| 94 |
-
return g
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
# %% 04. The time-conditioned residual block
|
| 98 |
-
#
|
| 99 |
-
# The step embedding enters as a per-channel offset between the two convolutions, so every layer
|
| 100 |
-
# operates with the current noise level available to it.
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
class ResidualBlock(nn.Module):
|
| 104 |
-
def __init__(self, in_channels, out_channels, time_embed_dim):
|
| 105 |
-
super().__init__()
|
| 106 |
-
self.norm1 = nn.GroupNorm(num_groups(in_channels), in_channels)
|
| 107 |
-
self.conv1 = nn.Conv2d(in_channels, out_channels,
|
| 108 |
-
kernel_size=3, padding=1)
|
| 109 |
-
self.time_proj = nn.Linear(time_embed_dim, out_channels)
|
| 110 |
-
self.norm2 = nn.GroupNorm(num_groups(out_channels), out_channels)
|
| 111 |
-
self.conv2 = nn.Conv2d(out_channels, out_channels,
|
| 112 |
-
kernel_size=3, padding=1)
|
| 113 |
-
self.skip = (
|
| 114 |
-
nn.Identity() if in_channels == out_channels
|
| 115 |
-
else nn.Conv2d(in_channels, out_channels, kernel_size=1)
|
| 116 |
-
)
|
| 117 |
-
self.activation = nn.SiLU()
|
| 118 |
-
|
| 119 |
-
def forward(self, x, t_emb):
|
| 120 |
-
h = self.conv1(self.activation(self.norm1(x))) # GN, SiLU, Conv_1.
|
| 121 |
-
h = h + self.time_proj(t_emb)[:, :, None, None] # Offset per channel.
|
| 122 |
-
h = self.conv2(self.activation(self.norm2(h)))
|
| 123 |
-
return h + self.skip(x) # Residual connection.
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
# %% 05. The U-Net
|
| 127 |
-
#
|
| 128 |
-
# Three scales, 28 by 28 down to 7 by 7 and back, with a skip at each resolution. The output has
|
| 129 |
-
# the shape of the input because it predicts one noise coordinate per pixel.
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
class UNet(nn.Module):
|
| 133 |
-
def __init__(self, in_channels=1, base_channels=64, time_embed_dim=128):
|
| 134 |
-
super().__init__()
|
| 135 |
-
self.time_embed_dim = time_embed_dim
|
| 136 |
-
self.time_mlp = nn.Sequential( # Learned representation of step k.
|
| 137 |
-
nn.Linear(time_embed_dim, time_embed_dim),
|
| 138 |
-
nn.SiLU(),
|
| 139 |
-
nn.Linear(time_embed_dim, time_embed_dim),
|
| 140 |
-
)
|
| 141 |
-
c1 = base_channels # 64 channels at 28 x 28.
|
| 142 |
-
c2 = base_channels * 2 # 128 channels at 14 x 14.
|
| 143 |
-
c3 = base_channels * 4 # 256 channels at 7 x 7.
|
| 144 |
-
|
| 145 |
-
self.in_conv = nn.Conv2d(in_channels, c1, kernel_size=3, padding=1)
|
| 146 |
-
self.down1 = ResidualBlock(c1, c1, time_embed_dim)
|
| 147 |
-
self.pool1 = nn.Conv2d(c1, c1, kernel_size=3, stride=2, padding=1)
|
| 148 |
-
self.down2 = ResidualBlock(c1, c2, time_embed_dim)
|
| 149 |
-
self.pool2 = nn.Conv2d(c2, c2, kernel_size=3, stride=2, padding=1)
|
| 150 |
-
self.mid = ResidualBlock(c2, c3, time_embed_dim)
|
| 151 |
-
self.up1 = nn.ConvTranspose2d(c3, c2, kernel_size=4,
|
| 152 |
-
stride=2, padding=1)
|
| 153 |
-
self.dec1 = ResidualBlock(c2 + c2, c2, time_embed_dim)
|
| 154 |
-
self.up2 = nn.ConvTranspose2d(c2, c1, kernel_size=4,
|
| 155 |
-
stride=2, padding=1)
|
| 156 |
-
self.dec2 = ResidualBlock(c1 + c1, c1, time_embed_dim)
|
| 157 |
-
self.out_conv = nn.Conv2d(c1, in_channels, kernel_size=1)
|
| 158 |
-
|
| 159 |
-
def forward(self, x, k):
|
| 160 |
-
t_emb = self.time_mlp(
|
| 161 |
-
timestep_embedding(k, self.time_embed_dim)
|
| 162 |
-
) # Learned representation of diffusion step k.
|
| 163 |
-
h = self.in_conv(x)
|
| 164 |
-
h1 = self.down1(h, t_emb) # 28 x 28 encoder features.
|
| 165 |
-
h = self.pool1(h1) # 28 -> 14.
|
| 166 |
-
h2 = self.down2(h, t_emb) # 14 x 14 encoder features.
|
| 167 |
-
h = self.pool2(h2) # 14 -> 7.
|
| 168 |
-
h = self.mid(h, t_emb) # Compressed representation.
|
| 169 |
-
h = self.up1(h) # 7 -> 14.
|
| 170 |
-
h = self.dec1(torch.cat([h, h2], dim=1), t_emb) # Join 14 x 14 skip.
|
| 171 |
-
h = self.up2(h) # 14 -> 28.
|
| 172 |
-
h = self.dec2(torch.cat([h, h1], dim=1), t_emb) # Join 28 x 28 skip.
|
| 173 |
-
return self.out_conv(h) # [B, 1, 28, 28] predicted noise.
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
# %% 06. One reverse step and the sampler
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
@torch.no_grad()
|
| 180 |
-
def p_sample(model, x_k, k):
|
| 181 |
-
eps_theta = model(x_k, k) # Predicted noise at step k.
|
| 182 |
-
beta_k = extract(betas, k, x_k.shape)
|
| 183 |
-
alpha_k = extract(alphas, k, x_k.shape)
|
| 184 |
-
abar_k = extract(alphas_cumprod, k, x_k.shape)
|
| 185 |
-
mean = (
|
| 186 |
-
x_k - beta_k / (1.0 - abar_k).sqrt() * eps_theta
|
| 187 |
-
) / alpha_k.sqrt() # Reverse mean mu_theta.
|
| 188 |
-
var = extract(posterior_variance, k, x_k.shape) # beta-tilde_k.
|
| 189 |
-
z = torch.randn_like(x_k) # Fresh Gaussian draw.
|
| 190 |
-
keep = (k > 1).to(torch.get_default_dtype()).reshape(-1, *((1,) * (x_k.dim() - 1)))
|
| 191 |
-
return mean + keep * var.sqrt() * z # No noise at the final step.
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
@torch.no_grad()
|
| 195 |
-
def sample(model, num_images, device):
|
| 196 |
-
x = torch.randn(num_images, 1, 28, 28, device=device) # X_K ~ N(0, I).
|
| 197 |
-
for step in range(NUM_STEPS, 0, -1): # k = K down to k = 1.
|
| 198 |
-
k = torch.full((num_images,), step, device=device, dtype=torch.long)
|
| 199 |
-
x = p_sample(model, x, k)
|
| 200 |
-
return x # Generated images in the training range [-1, 1].
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
# %% 07. The dropped weight and one complete reverse transition
|
| 204 |
-
#
|
| 205 |
-
# One interior term of the variational bound is beta_k^2 / (2 beta-tilde_k alpha_k (1 - abar_k))
|
| 206 |
-
# multiplied by the squared noise error, and L_simple sets every such factor to one. On the
|
| 207 |
-
# two-step schedule beta_1 = beta_2 = 0.2 the whole transition is small enough to check by hand,
|
| 208 |
-
# and the mean there has two equal forms, one built from the noise prediction and one from the
|
| 209 |
-
# clean estimate it implies.
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
def elbo_weight(beta_k, posterior, alpha_k, abar_k) -> float:
|
| 213 |
-
"""The schedule constant in front of the squared noise error at one step."""
|
| 214 |
-
return beta_k ** 2 / (2.0 * posterior * alpha_k * (1.0 - abar_k))
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
def two_step_transition(beta: float, x_k: float, eps_theta: float, z: float) -> dict:
|
| 218 |
-
"""The reverse step at k = 2 of the two-step schedule, through both forms of the mean."""
|
| 219 |
-
alpha = 1.0 - beta # alpha_2.
|
| 220 |
-
abar_1, abar_2 = alpha, alpha ** 2 # 0.8 and 0.64 at beta = 0.2.
|
| 221 |
-
sigma = math.sqrt(1.0 - abar_2) # 0.6, the corruption standard deviation at k = 2.
|
| 222 |
-
mean = (x_k - beta / sigma * eps_theta) / math.sqrt(alpha) # The noise form of mu_theta.
|
| 223 |
-
x_0_hat = (x_k - sigma * eps_theta) / math.sqrt(abar_2) # The clean estimate it implies.
|
| 224 |
-
weights = (math.sqrt(abar_1) * beta / (1.0 - abar_2), # Weight on the clean estimate.
|
| 225 |
-
math.sqrt(alpha) * (1.0 - abar_1) / (1.0 - abar_2)) # Weight on x_k.
|
| 226 |
-
variance = beta * (1.0 - abar_1) / (1.0 - abar_2) # beta-tilde_2 = 1/9.
|
| 227 |
-
return {
|
| 228 |
-
"mean_from_noise": mean, "mean_from_clean": weights[0] * x_0_hat + weights[1] * x_k,
|
| 229 |
-
"x_0_hat": x_0_hat, "weights": weights, "variance": variance,
|
| 230 |
-
"x_{k-1}": mean + math.sqrt(variance) * z,
|
| 231 |
-
"elbo_weight": elbo_weight(beta, variance, alpha, abar_2),
|
| 232 |
-
}
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
# %% 08. The report
|
| 236 |
-
def main() -> None:
|
| 237 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 238 |
-
parser.add_argument("--dataset", choices=["toy", "mnist"], default="toy")
|
| 239 |
-
parser.add_argument("--base-channels", type=int, default=8,
|
| 240 |
-
help="U-Net base width; the notes uses 64")
|
| 241 |
-
parser.set_defaults(steps=300, batch_size=64, lr=2e-3, samples=4)
|
| 242 |
-
args = parser.parse_args()
|
| 243 |
-
set_seed(args.seed)
|
| 244 |
-
device = pick_device(args.device)
|
| 245 |
-
|
| 246 |
-
if args.dataset == "toy":
|
| 247 |
-
from cis6270.data import toy_images
|
| 248 |
-
|
| 249 |
-
images = toy_images(512, 28, args.seed).to(device) # [512, 1, 28, 28] offline images.
|
| 250 |
-
draw = lambda n: images[torch.randint(0, len(images), (n,), device=device)] # noqa: E731
|
| 251 |
-
else:
|
| 252 |
-
from cis6270.data import mnist_loader
|
| 253 |
-
|
| 254 |
-
loader = mnist_loader(batch_size=args.batch_size)
|
| 255 |
-
stream = iter(loader)
|
| 256 |
-
|
| 257 |
-
def draw(n): # One MNIST batch per call, restarting the loader at the end of an epoch.
|
| 258 |
-
nonlocal stream
|
| 259 |
-
batch = next(stream, None)
|
| 260 |
-
if batch is None:
|
| 261 |
-
stream = iter(loader)
|
| 262 |
-
batch = next(stream)
|
| 263 |
-
return batch[0].to(device)
|
| 264 |
-
|
| 265 |
-
model = UNet(in_channels=1, base_channels=args.base_channels, time_embed_dim=128).to(device)
|
| 266 |
-
if not args.quiet:
|
| 267 |
-
print(f"device={device} dataset={args.dataset} parameters={count_parameters(model):,}")
|
| 268 |
-
|
| 269 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 270 |
-
x_0 = draw(args.batch_size) # [B, 1, 28, 28] clean images in [-1, 1].
|
| 271 |
-
k = torch.randint(1, NUM_STEPS + 1, (x_0.shape[0],), device=device) # One level per item.
|
| 272 |
-
x_k, noise = q_sample(x_0, k) # Corrupt directly to step k.
|
| 273 |
-
return F.mse_loss(model(x_k, k), noise) # L_simple on this batch.
|
| 274 |
-
|
| 275 |
-
# The notes trains at 2e-4 for 20 epochs; 2e-3 for a few hundred steps is the
|
| 276 |
-
# classroom version of the same run and reaches a usable sampler in under a minute.
|
| 277 |
-
losses = train(model, loss_fn, args.steps, args.lr, quiet=args.quiet)
|
| 278 |
-
images_out = sample(model, args.samples, device) # [S, 1, 28, 28] after K reverse steps.
|
| 279 |
-
|
| 280 |
-
window = min(100, args.steps) # A short run averages over everything it has.
|
| 281 |
-
reverse = two_step_transition(0.2, 7.4, -1.0, 0.3)
|
| 282 |
-
weight_500 = elbo_weight(betas[500], posterior_variance[500],
|
| 283 |
-
alphas[500], alphas_cumprod[500]) # The dropped factor at k = 500.
|
| 284 |
-
report = {
|
| 285 |
-
"parameters": count_parameters(model),
|
| 286 |
-
f"loss, first {window} steps": sum(losses[:window]) / window,
|
| 287 |
-
f"loss, last {window} steps": sum(losses[-window:]) / window,
|
| 288 |
-
"beta-tilde at k=500": f"{float(posterior_variance[500]):.5f}", # 0.01003.
|
| 289 |
-
# The weight L_simple drops, on the thousand-step grid and on the two-step hand schedule.
|
| 290 |
-
"ELBO weight, k=500 and two-step": [round(float(weight_500), 6),
|
| 291 |
-
round(reverse["elbo_weight"], 4)], # 0.005508, 0.625.
|
| 292 |
-
"two-step reverse mean, two forms": [round(reverse["mean_from_noise"], 4),
|
| 293 |
-
round(reverse["mean_from_clean"], 4)], # 8.646.
|
| 294 |
-
"two-step x_0 estimate and weights":
|
| 295 |
-
[round(reverse["x_0_hat"], 4)] + [round(w, 4) for w in reverse["weights"]],
|
| 296 |
-
"two-step reverse variance and x_1": [round(reverse["variance"], 4),
|
| 297 |
-
round(reverse["x_{k-1}"], 4)], # 1/9 and 8.746.
|
| 298 |
-
"generated range": [round(float(images_out.min()), 3), round(float(images_out.max()), 3)],
|
| 299 |
-
"generated standard deviation": float(images_out.std()),
|
| 300 |
-
}
|
| 301 |
-
print_report(f"DDPM on {args.dataset} images", report)
|
| 302 |
-
saved = save_run(args.out, model=model, config=vars(args), report=report, losses=losses)
|
| 303 |
-
if saved is not None:
|
| 304 |
-
try: # Without torchvision the run still trains, samples and saves its weights.
|
| 305 |
-
from torchvision.utils import save_image
|
| 306 |
-
|
| 307 |
-
display = ((images_out.cpu() + 1.0) / 2.0).clamp(0.0, 1.0) # [-1, 1] -> [0, 1].
|
| 308 |
-
save_image(display, saved / "samples.png", nrow=4) # A grid of the generated images.
|
| 309 |
-
except ImportError:
|
| 310 |
-
pass
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
# %% 09. Run the script
|
| 314 |
-
if __name__ == "__main__":
|
| 315 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_3/forward_diffusion.py
DELETED
|
@@ -1,204 +0,0 @@
|
|
| 1 |
-
"""The schedule and the Gaussian corruption it drives.
|
| 2 |
-
|
| 3 |
-
The variance-preserving path is fixed once a noising rate is chosen. Its accumulated rate gives a
|
| 4 |
-
signal coefficient and a noise standard deviation at every level, a thousand-step grid tracks the
|
| 5 |
-
continuous curve to three decimal places, and one Gaussian draw is enough to reach any level from
|
| 6 |
-
a clean image. Nothing here learns anything, so every number is arithmetic or a sample average
|
| 7 |
-
against a law written in closed form.
|
| 8 |
-
|
| 9 |
-
Chapter 3 runs its diffusion clock from the data, so step k = 0 holds a clean image and corruption
|
| 10 |
-
grows with k, up to pure noise at k = K.
|
| 11 |
-
|
| 12 |
-
Chapter 3, Sections 3.11 and 3.12 of the notes. Runs on the offline 28-by-28 images. Prints the
|
| 13 |
-
four noise levels of the schedule, the discrete grid against the continuous curve, the chapter's
|
| 14 |
-
one pixel at step 207, the worked training batch at abar = 0.8, and the corruption law measured on
|
| 15 |
-
a batch.
|
| 16 |
-
|
| 17 |
-
python lecture_3/forward_diffusion.py # 16 levels, 1024 draws at each
|
| 18 |
-
python lecture_3/forward_diffusion.py --steps 20 # a few seconds, for a first look
|
| 19 |
-
"""
|
| 20 |
-
|
| 21 |
-
# %% 01. Imports and configuration
|
| 22 |
-
import math
|
| 23 |
-
|
| 24 |
-
import torch
|
| 25 |
-
|
| 26 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed
|
| 27 |
-
|
| 28 |
-
NUM_STEPS = 1000 # Diffusion steps K.
|
| 29 |
-
BETA_START = 1e-4 # Noise variance added at step 1.
|
| 30 |
-
BETA_END = 0.02 # Noise variance added at step K.
|
| 31 |
-
BETA_MIN = 0.1 # beta(0) of the continuous schedule these endpoints discretize.
|
| 32 |
-
BETA_MAX = 20.0 # beta(1) of the same schedule.
|
| 33 |
-
|
| 34 |
-
# `--steps` is the number of levels at which the corruption law is measured and `--samples` the
|
| 35 |
-
# number of draws taken at each of them; nothing in this file is trained.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
# %% 02. The schedule tensors
|
| 39 |
-
#
|
| 40 |
-
# alpha_k = 1 - beta_k is the one-step retention and abar_k its running product. A zero is prefixed
|
| 41 |
-
# to the betas so that Python index k holds beta_k and the cumulative product starts at abar_0 = 1,
|
| 42 |
-
# the clean state. The two corruption coefficients are the square roots of abar_k and 1 - abar_k.
|
| 43 |
-
|
| 44 |
-
betas = torch.cat([
|
| 45 |
-
torch.zeros(1), # Placeholder so that index k holds beta_k.
|
| 46 |
-
torch.linspace(BETA_START, BETA_END, NUM_STEPS), # beta_1 ... beta_K.
|
| 47 |
-
])
|
| 48 |
-
alphas = 1.0 - betas # One-step signal retention alpha_k.
|
| 49 |
-
alphas_cumprod = torch.cumprod(alphas, dim=0) # Cumulative retention abar_k.
|
| 50 |
-
a = alphas_cumprod.sqrt() # Signal coefficient a_k.
|
| 51 |
-
sigma = (1.0 - alphas_cumprod).sqrt() # Noise standard deviation sigma_k.
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
# %% 03. Forward corruption
|
| 55 |
-
#
|
| 56 |
-
# x_k = a_k x_0 + sigma_k eps, the Gaussian transition q(x_k | x_0) = N(a_k x_0, sigma_k^2 I) read
|
| 57 |
-
# off at step k. One draw reaches any level, so training never simulates the path.
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def extract(schedule, k, shape):
|
| 61 |
-
# One schedule value per item, reshaped to broadcast over C, H, W.
|
| 62 |
-
out = schedule.to(k.device).gather(0, k)
|
| 63 |
-
return out.reshape(k.shape[0], *((1,) * (len(shape) - 1)))
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def q_sample(x_0, k, noise=None):
|
| 67 |
-
if noise is None:
|
| 68 |
-
noise = torch.randn_like(x_0) # Mean 0, covariance I, shape of x_0.
|
| 69 |
-
abar = extract(alphas_cumprod, k, x_0.shape)
|
| 70 |
-
a_k = abar.sqrt() # Signal coefficient a_k.
|
| 71 |
-
sigma_k = (1.0 - abar).sqrt() # Noise standard deviation sigma_k.
|
| 72 |
-
return a_k * x_0 + sigma_k * noise, noise # x_k and the exact noise.
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
# %% 04. The continuous schedule behind the grid
|
| 76 |
-
#
|
| 77 |
-
# beta(t) = 0.1 + 19.9 t integrates to B(t) = 0.1 t + 9.95 t^2 and a_t = exp(-B(t)/2). Dividing the
|
| 78 |
-
# per-step variances 1e-4 and 0.02 by the step size 1/1000 recovers the two endpoints, so the grid
|
| 79 |
-
# and the curve describe one path. Inverting a_t = exp(-B(t)/2) locates a chosen noise level.
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def noising_rate(t: float) -> float:
|
| 83 |
-
"""beta(t) = beta_min + (beta_max - beta_min) t, the linear noising rate."""
|
| 84 |
-
return BETA_MIN + (BETA_MAX - BETA_MIN) * t
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
def continuous_signal(t: float) -> float:
|
| 88 |
-
"""a_t = exp(-B(t)/2) with B(t) = 0.1 t + 9.95 t^2, the curve the discrete betas track."""
|
| 89 |
-
return math.exp(-(BETA_MIN * t + 0.5 * (BETA_MAX - BETA_MIN) * t ** 2) / 2.0)
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def level_time(a_t: float) -> dict:
|
| 93 |
-
"""Solve B(t) = -2 log a_t for the time at which the signal coefficient is a_t.
|
| 94 |
-
|
| 95 |
-
The quadratic 9.95 t^2 + 0.1 t - B = 0 has one positive root, which is the time the notes
|
| 96 |
-
tabulates beside the noise standard deviation and the noising rate there.
|
| 97 |
-
"""
|
| 98 |
-
accumulated = -2.0 * math.log(a_t) # B(t) at the requested level.
|
| 99 |
-
quadratic = 0.5 * (BETA_MAX - BETA_MIN) # Coefficient of t^2 in B(t).
|
| 100 |
-
t = (-BETA_MIN + math.sqrt(BETA_MIN ** 2 + 4.0 * quadratic * accumulated)) / (2.0 * quadratic)
|
| 101 |
-
return {"t": t, "sigma_t": math.sqrt(1.0 - a_t ** 2), "beta_t": noising_rate(t)}
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
# %% 05. One training batch at the running level
|
| 105 |
-
#
|
| 106 |
-
# Three clean scalars at a level with abar = 0.8 give the batch the notes carries through the
|
| 107 |
-
# denoising loss by hand: the corrupted values, the squared error against a supposed prediction,
|
| 108 |
-
# the score targets the noise regression never forms, and the clean estimates the prediction
|
| 109 |
-
# implies.
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def worked_batch(abar: float, x_0, epsilon, predicted) -> dict:
|
| 113 |
-
"""One training batch at a level with the given cumulative retention, as the notes works it."""
|
| 114 |
-
a_k, sigma_k = math.sqrt(abar), math.sqrt(1.0 - abar) # 0.8944 and 0.4472 at abar = 0.8.
|
| 115 |
-
x_0, epsilon, predicted = (torch.tensor(v) for v in (x_0, epsilon, predicted))
|
| 116 |
-
x_k = a_k * x_0 + sigma_k * epsilon # [3] the corrupted batch.
|
| 117 |
-
return {
|
| 118 |
-
"a_k": a_k, "sigma_k": sigma_k,
|
| 119 |
-
"x_k": [round(float(v), 4) for v in x_k],
|
| 120 |
-
"loss": float((predicted - epsilon).square().mean()), # L_simple on this batch.
|
| 121 |
-
"score_targets": [round(float(-e / sigma_k), 3) for e in epsilon],
|
| 122 |
-
"x_0_hat": [round(float((x - sigma_k * p) / a_k), 2) for x, p in zip(x_k, predicted)],
|
| 123 |
-
}
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
# %% 06. The corruption law, measured
|
| 127 |
-
#
|
| 128 |
-
# q(x_k | x_0) is Gaussian with mean a_k x_0 and variance sigma_k^2 in every coordinate. Corrupting
|
| 129 |
-
# one clean image many times at a fixed level and averaging over the draws therefore recovers
|
| 130 |
-
# a_k x_0 pixel by pixel, and the spread around it recovers sigma_k^2. The largest gap over a
|
| 131 |
-
# spread of levels falls as one over the square root of the number of draws, and a wrong
|
| 132 |
-
# coefficient in either of the two lines of `q_sample` would leave a gap that does not.
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def measure_law(x_0, levels, repeats: int, generator=None) -> dict:
|
| 136 |
-
"""Corrupt one clean image ``repeats`` times at each level and compare with q(x_k | x_0)."""
|
| 137 |
-
batch = x_0.expand(repeats, *x_0.shape[1:]) # [R, 1, 28, 28] one image, repeated.
|
| 138 |
-
mean_gap, variance_gap = 0.0, 0.0
|
| 139 |
-
for step in levels:
|
| 140 |
-
k = torch.full((repeats,), int(step), dtype=torch.long) # [R] the same level throughout.
|
| 141 |
-
noise = torch.randn(batch.shape, generator=generator) # [R, 1, 28, 28] fresh draws.
|
| 142 |
-
x_k, _ = q_sample(batch, k, noise) # [R, 1, 28, 28] corrupted at this level.
|
| 143 |
-
centre = float(a[step]) * x_0[0] # [1, 28, 28] the conditional mean a_k x_0.
|
| 144 |
-
mean_gap = max(mean_gap, float((x_k.mean(dim=0) - centre).abs().mean()))
|
| 145 |
-
spread = float(x_k.var(dim=0, unbiased=False).mean()) # Pooled over the pixels.
|
| 146 |
-
variance_gap = max(variance_gap, abs(spread - float(sigma[step]) ** 2))
|
| 147 |
-
return {"mean_gap": mean_gap, "variance_gap": variance_gap}
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
# %% 07. The report
|
| 151 |
-
def main() -> None:
|
| 152 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 153 |
-
parser.set_defaults(steps=16, samples=1024)
|
| 154 |
-
args = parser.parse_args()
|
| 155 |
-
set_seed(args.seed)
|
| 156 |
-
device = pick_device(args.device) # Printed for consistency; this file runs on the CPU.
|
| 157 |
-
generator = torch.Generator().manual_seed(args.seed) # Local generator, so the report repeats.
|
| 158 |
-
|
| 159 |
-
from cis6270.data import toy_images
|
| 160 |
-
|
| 161 |
-
image = toy_images(1, 28, args.seed) # [1, 1, 28, 28] one offline image in [-1, 1].
|
| 162 |
-
levels = torch.linspace(1, NUM_STEPS, args.steps).round().long().tolist() # Spread over K.
|
| 163 |
-
law = measure_law(image, levels, args.samples, generator)
|
| 164 |
-
|
| 165 |
-
# The four levels of the notes' table, located by inverting a_t = exp(-B(t)/2).
|
| 166 |
-
located = [level_time(level) for level in (0.98, 0.8, 0.35, 0.04)]
|
| 167 |
-
crossing = level_time(1.0 / math.sqrt(2.0)) # Where a_t and sigma_t meet.
|
| 168 |
-
batch = worked_batch(0.8, (2.0, -1.0, 0.5), (0.5, -0.2, 1.0), (0.4, -0.1, 0.8))
|
| 169 |
-
|
| 170 |
-
report = {
|
| 171 |
-
"levels measured, draws each": [len(levels), args.samples],
|
| 172 |
-
"largest gap, sample mean": law["mean_gap"], # Zero up to the sampling error.
|
| 173 |
-
"largest gap, sample variance": law["variance_gap"],
|
| 174 |
-
"times at a_t = 0.98, 0.8, 0.35, 0.04": [round(v["t"], 4) for v in located],
|
| 175 |
-
"sigma_t at the same four levels": [round(v["sigma_t"], 4) for v in located],
|
| 176 |
-
"beta(t) at the same four levels": [round(v["beta_t"], 4) for v in located],
|
| 177 |
-
# Where the two curves cross, and the value they share there.
|
| 178 |
-
"crossing time and a_t = sigma_t": [round(crossing["t"], 5),
|
| 179 |
-
round(crossing["sigma_t"], 4)], # 0.25896, 0.7071.
|
| 180 |
-
# The discrete coefficient against the continuous curve, at step 207 and at the last step.
|
| 181 |
-
"abar_207": round(float(alphas_cumprod[207]), 4), # 0.6402.
|
| 182 |
-
"a_207 discrete and continuous": [round(float(a[207]), 4),
|
| 183 |
-
round(continuous_signal(0.207), 4)], # 0.8001, 0.7997.
|
| 184 |
-
"sigma_207 discrete and continuous": [round(float(sigma[207]), 4),
|
| 185 |
-
round(math.sqrt(1.0 - continuous_signal(0.207) ** 2),
|
| 186 |
-
4)], # 0.5999, 0.6004.
|
| 187 |
-
"a_1000 discrete and continuous": [round(float(a[1000]), 4),
|
| 188 |
-
round(continuous_signal(1.0), 4)], # 0.0064, 0.0066.
|
| 189 |
-
"one pixel at k=207, x_0=0.5, eps=-1": float(a[207] * 0.5 - sigma[207]), # -0.1998.
|
| 190 |
-
"a_500, where noise exceeds signal": round(float(a[500]), 4), # 0.2803.
|
| 191 |
-
"worked batch at abar=0.8, x_k": batch["x_k"], # 2.0125, -0.9839, 0.8944.
|
| 192 |
-
"worked batch loss": batch["loss"], # 0.02.
|
| 193 |
-
"worked batch score targets": batch["score_targets"], # -1.118, 0.447, -2.236.
|
| 194 |
-
"worked batch x_0 estimates": batch["x_0_hat"], # 2.05, -1.05, 0.60.
|
| 195 |
-
}
|
| 196 |
-
if not args.quiet:
|
| 197 |
-
print(f"device={device} levels={args.steps} draws={args.samples} seed={args.seed}")
|
| 198 |
-
print_report("The variance-preserving corruption", report)
|
| 199 |
-
save_run(args.out, config=vars(args), report=report)
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
# %% 08. Run the script
|
| 203 |
-
if __name__ == "__main__":
|
| 204 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_3/guidance.py
DELETED
|
@@ -1,280 +0,0 @@
|
|
| 1 |
-
"""Steering a sampler by adding one gradient to the score.
|
| 2 |
-
|
| 3 |
-
Bayes' rule splits the conditional score into the unconditional score plus the gradient of the log
|
| 4 |
-
likelihood of the condition, so every form of control in this chapter is one extra term in the
|
| 5 |
-
same place. A classifier supplies that term directly, a conditional and an unconditional
|
| 6 |
-
prediction of one network supply it as their difference, and a differentiable reward supplies it
|
| 7 |
-
as its own gradient, with several objectives combined through weights on the simplex. The target
|
| 8 |
-
is the four-Gaussian mixture with one label per mode, where every score is a closed form, so the
|
| 9 |
-
guidance weight shows up directly in the mode fractions.
|
| 10 |
-
|
| 11 |
-
Chapter 3 runs its diffusion clock from the data, so t = 0 holds a clean sample and corruption
|
| 12 |
-
grows with t. Generation traverses the same interval from t = 1 back to t = 0.
|
| 13 |
-
|
| 14 |
-
Chapter 3, Sections 3.37 through 3.45 of the notes. Prints the mode fractions at several guidance
|
| 15 |
-
weights, the chapter's worked guided score and guided backward step, the two candidates under two
|
| 16 |
-
weightings, and the schedule value at t = 0.6.
|
| 17 |
-
|
| 18 |
-
python lecture_3/guidance.py # 200 reverse steps per setting
|
| 19 |
-
python lecture_3/guidance.py --steps 20 # a coarse grid, for a first look
|
| 20 |
-
"""
|
| 21 |
-
|
| 22 |
-
# %% 01. Imports and configuration
|
| 23 |
-
import math
|
| 24 |
-
|
| 25 |
-
import torch
|
| 26 |
-
|
| 27 |
-
from cis6270.data import GAUSSIAN_CENTERS, GAUSSIAN_STD
|
| 28 |
-
from cis6270.metrics import mode_report
|
| 29 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed
|
| 30 |
-
|
| 31 |
-
BETA_MIN = 0.1 # beta(0) of the linear variance-preserving schedule.
|
| 32 |
-
BETA_MAX = 20.0 # beta(1) of the same schedule.
|
| 33 |
-
TARGET_LABEL = 0 # The mode at (2, 2), which every guided run is steered toward.
|
| 34 |
-
REWARD_SCALE = 2.0 # Distance from the origin to a mode, used to standardize the two rewards.
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
# %% 02. The target, its labels and the exact scores
|
| 38 |
-
#
|
| 39 |
-
# Each mode carries its own label, so p_t(x | c) is a single Gaussian with mean a_t mu_c and
|
| 40 |
-
# variance a_t^2 s^2 + sigma_t^2, and p_t(x) is the equal mixture of the four. Both scores are
|
| 41 |
-
# closed forms, and nothing in this file is trained.
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def schedule_tensors(num_steps: int) -> dict:
|
| 45 |
-
"""The DDPM tensors on a grid of K steps, read off abar_k = exp(-B(t_k))."""
|
| 46 |
-
grid = (torch.arange(0, num_steps + 1).to(torch.get_default_dtype())
|
| 47 |
-
/ num_steps) # [K+1] the times t_k.
|
| 48 |
-
alphas_cumprod = torch.exp(-(BETA_MIN * grid + 0.5 * (BETA_MAX - BETA_MIN) * grid ** 2))
|
| 49 |
-
previous = torch.cat([torch.ones(1), alphas_cumprod[:-1]]) # abar_{k-1}.
|
| 50 |
-
alphas = alphas_cumprod / previous # One-step retention alpha_k.
|
| 51 |
-
betas = 1.0 - alphas # Per-step variance beta_k.
|
| 52 |
-
return {"betas": betas, "alphas": alphas, "alphas_cumprod": alphas_cumprod,
|
| 53 |
-
"posterior_variance": betas * (1.0 - previous) / (1.0 - alphas_cumprod)}
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def component_log_weights(x: torch.Tensor, a_t: float):
|
| 57 |
-
"""Log of each mode's unnormalized contribution at x, and the offsets to the four means."""
|
| 58 |
-
variance = a_t ** 2 * GAUSSIAN_STD ** 2 + (1.0 - a_t ** 2) # Width of p_t.
|
| 59 |
-
offset = x[:, None, :] - a_t * GAUSSIAN_CENTERS.to(x)[None, :, :] # [B, 4, 2].
|
| 60 |
-
return -(offset ** 2).sum(dim=2) / (2.0 * variance), offset, variance
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def unconditional_score(x: torch.Tensor, a_t: float) -> torch.Tensor:
|
| 64 |
-
"""The score of the equal mixture at level a_t. [B, 2] -> [B, 2]."""
|
| 65 |
-
log_weight, offset, variance = component_log_weights(x, a_t)
|
| 66 |
-
responsibility = log_weight.softmax(dim=1)[:, :, None] # [B, 4, 1] posterior over the modes.
|
| 67 |
-
return (responsibility * (-offset / variance)).sum(dim=1) # [B, 2].
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def conditional_score(x: torch.Tensor, a_t: float, label: int) -> torch.Tensor:
|
| 71 |
-
"""The score of p_t(. | c), a single Gaussian centered on a_t mu_c. [B, 2] -> [B, 2]."""
|
| 72 |
-
_, offset, variance = component_log_weights(x, a_t)
|
| 73 |
-
return -offset[:, label, :] / variance # [B, 2].
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
# %% 03. Conditioning the score and the guidance scale
|
| 77 |
-
#
|
| 78 |
-
# s_t(x | c) = s_t(x) + grad log p_t(c | x), and attaching a scale gamma to the second term samples
|
| 79 |
-
# the tilted density p_t(x) p_t(c | x)^gamma. At gamma = 0 generation is unconditional, at
|
| 80 |
-
# gamma = 1 it follows Bayes' rule, and at gamma > 1 the condition is weighted more heavily.
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def classifier_gradient(x: torch.Tensor, a_t: float, label: int) -> torch.Tensor:
|
| 84 |
-
"""grad log p_t(c | x), taken as the difference of the two scores. [B, 2] -> [B, 2]."""
|
| 85 |
-
return conditional_score(x, a_t, label) - unconditional_score(x, a_t)
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def guided_score(score: torch.Tensor, gradient: torch.Tensor, gamma: float) -> torch.Tensor:
|
| 89 |
-
"""s_theta + gamma grad log p_phi(c | x), the guided score of the notes."""
|
| 90 |
-
return score + gamma * gradient
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
def guided_noise(eps: torch.Tensor, gradient: torch.Tensor, gamma: float, sigma_k: float):
|
| 94 |
-
"""eps_theta - gamma sigma_t grad log p_phi(c | x), the same guidance in the noise."""
|
| 95 |
-
return eps - gamma * sigma_k * gradient
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
def classifier_free_field(unconditional, conditional, w: float):
|
| 99 |
-
"""F_uncond + w (F_cond - F_uncond), which needs no separate classifier."""
|
| 100 |
-
return unconditional + w * (conditional - unconditional)
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
# %% 04. Reward guidance and several objectives at once
|
| 104 |
-
#
|
| 105 |
-
# A reward is a differentiable scalar that is larger for more desirable states, and exponential
|
| 106 |
-
# reweighting turns it into a density whose score is s_t(x) + eta grad R_t(x). With M objectives,
|
| 107 |
-
# the weights are clamped to be nonnegative and divided by their sum, which is the simplex
|
| 108 |
-
# condition of the notes, and the gradient is linear in them.
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def reward_model(z: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 112 |
-
"""Two standardized properties of the state, its reach to the right and its reach upward.
|
| 113 |
-
|
| 114 |
-
The notes train a small network on the latent and the time here. Two coordinates of a planar
|
| 115 |
-
state need no network, so the properties are read off directly and keep the same signature.
|
| 116 |
-
"""
|
| 117 |
-
return z / REWARD_SCALE # [B, 2] one standardized property per coordinate.
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
def normalize_weights(lambdas, device):
|
| 121 |
-
"""Clamp the weights to be nonnegative and divide by their sum, the simplex condition."""
|
| 122 |
-
w = torch.as_tensor(lambdas, dtype=torch.get_default_dtype(), device=device)
|
| 123 |
-
w = w.clamp_min(0.0)
|
| 124 |
-
return w / w.sum().clamp_min(1e-8)
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
def weighted_reward(reward_model, z, t, lambdas):
|
| 128 |
-
"""R_lambda(z) = sum_m lambda_m r_m(z), one scalar per batch item."""
|
| 129 |
-
r_hat = reward_model(z, t) # [B, M]
|
| 130 |
-
return (r_hat * normalize_weights(lambdas, z.device)).sum(dim=1)
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
def reward_gradient(reward_model, z, t, lambdas):
|
| 134 |
-
"""grad_z R_lambda(z), taken inside a no-grad sampler. [B, d] -> [B, d]."""
|
| 135 |
-
with torch.enable_grad():
|
| 136 |
-
state = z.detach().requires_grad_(True)
|
| 137 |
-
R = weighted_reward(reward_model, state, t, lambdas).sum()
|
| 138 |
-
grad, = torch.autograd.grad(R, state)
|
| 139 |
-
return grad.detach()
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
def scalarize(values, lambdas) -> float:
|
| 143 |
-
"""The weighted sum of normalized objectives, R_lambda(x), on one candidate."""
|
| 144 |
-
weights = normalize_weights(lambdas, torch.device("cpu"))
|
| 145 |
-
return float((torch.as_tensor(values, dtype=torch.get_default_dtype()) * weights).sum())
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
# %% 05. Scheduled guidance
|
| 149 |
-
#
|
| 150 |
-
# A property predictor is not equally reliable at every noise level, so the strength is allowed to
|
| 151 |
-
# depend on time, gamma(t) = gamma_max zeta(t). The schedule zeta(t) = 4 t (1 - t) vanishes at both
|
| 152 |
-
# ends of the interval and peaks at one in the middle.
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
def schedule_value(t: float) -> float:
|
| 156 |
-
"""zeta(t) = 4 t (1 - t), between zero and one."""
|
| 157 |
-
return 4.0 * t * (1.0 - t)
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
# %% 06. The guided sampler
|
| 161 |
-
#
|
| 162 |
-
# The reverse step is the one of Section 3.26 with the guided noise prediction in place of the
|
| 163 |
-
# raw one. Classifier-free guidance is applied first, then the reward term, which is the order the
|
| 164 |
-
# notes implements.
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
@torch.no_grad()
|
| 168 |
-
def sample_diffusion(schedule, num_points, label, w=0.0, eta=0.0, lambdas=(1.0, 0.0),
|
| 169 |
-
scheduled=False, generator=None) -> torch.Tensor:
|
| 170 |
-
"""Draw points under classifier-free guidance toward ``label`` and reward steering."""
|
| 171 |
-
num_steps = len(schedule["betas"]) - 1
|
| 172 |
-
z = torch.randn(num_points, 2, generator=generator) # [N, 2] the terminal Gaussian.
|
| 173 |
-
for step in range(num_steps, 0, -1): # k = K down to k = 1.
|
| 174 |
-
t = step / num_steps # Normalized time, the input the network would receive.
|
| 175 |
-
abar = float(schedule["alphas_cumprod"][step])
|
| 176 |
-
a_k, sigma_k = math.sqrt(abar), math.sqrt(1.0 - abar)
|
| 177 |
-
eps = -sigma_k * unconditional_score(z, a_k) # [N, 2] unconditional noise.
|
| 178 |
-
if w > 0.0:
|
| 179 |
-
conditional = -sigma_k * conditional_score(z, a_k, label) # [N, 2].
|
| 180 |
-
strength = w * (schedule_value(t) if scheduled else 1.0) # gamma(t) = gamma_max zeta.
|
| 181 |
-
eps = classifier_free_field(eps, conditional, strength)
|
| 182 |
-
if eta > 0.0:
|
| 183 |
-
grad_R = reward_gradient(reward_model, z, torch.full((num_points,), t), lambdas)
|
| 184 |
-
eps = eps - eta * sigma_k * grad_R # Reward guidance in the noise parameterization.
|
| 185 |
-
beta_k, alpha_k = float(schedule["betas"][step]), float(schedule["alphas"][step])
|
| 186 |
-
mean = (z - beta_k / sigma_k * eps) / math.sqrt(alpha_k) # Reverse mean mu_theta.
|
| 187 |
-
if step > 1:
|
| 188 |
-
variance = float(schedule["posterior_variance"][step])
|
| 189 |
-
z = mean + math.sqrt(variance) * torch.randn(z.shape, generator=generator)
|
| 190 |
-
else:
|
| 191 |
-
z = mean
|
| 192 |
-
return z # [N, 2].
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
# %% 07. The report
|
| 196 |
-
def main() -> None:
|
| 197 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 198 |
-
parser.set_defaults(steps=200, samples=2048)
|
| 199 |
-
args = parser.parse_args()
|
| 200 |
-
set_seed(args.seed)
|
| 201 |
-
device = pick_device(args.device) # Two coordinates per point; a CPU is enough.
|
| 202 |
-
schedule = schedule_tensors(args.steps)
|
| 203 |
-
|
| 204 |
-
def run(**settings) -> list:
|
| 205 |
-
generator = torch.Generator().manual_seed(args.seed) # The same starting noise each time.
|
| 206 |
-
points = sample_diffusion(schedule, args.samples, TARGET_LABEL, generator=generator,
|
| 207 |
-
**settings)
|
| 208 |
-
return mode_report(points)["mode_fractions"]
|
| 209 |
-
|
| 210 |
-
# The conditional score here is exact, so at w = 1 the condition already pins the mode and
|
| 211 |
-
# the fractions saturate. The graded part of the sweep therefore sits below one.
|
| 212 |
-
report = {}
|
| 213 |
-
for w in (0.0, 0.1, 0.25, 0.5, 1.0): # The target mode is the first entry of each list.
|
| 214 |
-
report[f"mode fractions at w={w}"] = run(w=w)
|
| 215 |
-
report["mode fractions, w=1 scheduled"] = run(w=1.0, scheduled=True)
|
| 216 |
-
for eta in (0.5, 2.0): # Reward steering toward the two right-hand modes.
|
| 217 |
-
report[f"mode fractions at eta={eta}, lambda=(1,0)"] = run(eta=eta, lambdas=(1.0, 0.0))
|
| 218 |
-
report["mode fractions at eta=2, lambda=(0.7,0.3)"] = run(eta=2.0, lambdas=(0.7, 0.3))
|
| 219 |
-
|
| 220 |
-
# The two routes to one guided noise prediction, on a batch at a middle level.
|
| 221 |
-
probe = torch.randn(256, 2, generator=torch.Generator().manual_seed(args.seed + 1))
|
| 222 |
-
a_t, sigma_t = 0.8, 0.6
|
| 223 |
-
by_classifier = guided_noise(-sigma_t * unconditional_score(probe, a_t),
|
| 224 |
-
classifier_gradient(probe, a_t, TARGET_LABEL), 2.0, sigma_t)
|
| 225 |
-
by_difference = classifier_free_field(-sigma_t * unconditional_score(probe, a_t),
|
| 226 |
-
-sigma_t * conditional_score(probe, a_t, TARGET_LABEL),
|
| 227 |
-
2.0)
|
| 228 |
-
gap = float((by_classifier - by_difference).abs().max())
|
| 229 |
-
report["classifier and classifier-free gap"] = gap
|
| 230 |
-
|
| 231 |
-
# The chapter's worked numbers, none of which needs a sampler.
|
| 232 |
-
score, gradient = torch.tensor([[-1.0, -0.1]]), torch.tensor([[1.3, 1.1]])
|
| 233 |
-
for gamma in (1.0, 2.0):
|
| 234 |
-
direction = guided_score(score, gradient, gamma)[0]
|
| 235 |
-
report[f"guided direction at gamma={gamma}"] = [round(float(v), 4) for v in direction]
|
| 236 |
-
report[f"its length and angle at gamma={gamma}"] = [
|
| 237 |
-
round(float(direction.norm()), 4),
|
| 238 |
-
round(math.degrees(math.atan2(float(direction[1]), float(direction[0]))), 1),
|
| 239 |
-
]
|
| 240 |
-
cfg_eps = float(classifier_free_field(torch.tensor(0.50), torch.tensor(0.10), 2.0))
|
| 241 |
-
report["classifier-free eps at w=2"] = cfg_eps # -0.30, past the conditional 0.10.
|
| 242 |
-
report["its implied score"] = -cfg_eps / 0.5 # 0.6, at sigma_t = 0.5.
|
| 243 |
-
|
| 244 |
-
# One guided backward step from the state of the deterministic sampler, through both
|
| 245 |
-
# parameterizations. A disagreement between them is a missing sigma_t or a missing one half.
|
| 246 |
-
x_t, beta_t, sigma_t, eps_theta = 1.2, 2.0, 0.5, 0.8 # The state of Section 3.35.
|
| 247 |
-
gradient, gamma, dt = 0.6, 2.0, 0.1 # The classifier gradient, the scale and the step.
|
| 248 |
-
unguided = -0.5 * beta_t * (x_t - eps_theta / sigma_t) # 0.4, the unguided velocity.
|
| 249 |
-
guided = unguided - 0.5 * gamma * beta_t * gradient # -0.8, half of gamma g^2 on the gradient.
|
| 250 |
-
guided_eps = float(guided_noise(torch.tensor(eps_theta), torch.tensor(gradient),
|
| 251 |
-
gamma, sigma_t)) # 0.2, the same guidance in the noise.
|
| 252 |
-
report["guided velocity and next state"] = [round(guided, 4),
|
| 253 |
-
round(x_t - dt * guided, 4)] # -0.8 and 1.28.
|
| 254 |
-
report["guided eps and its velocity"] = [
|
| 255 |
-
round(guided_eps, 4), # 0.2, whose implied score is -0.4.
|
| 256 |
-
round(-0.5 * beta_t * (x_t - guided_eps / sigma_t), 4), # -0.8, the same velocity.
|
| 257 |
-
]
|
| 258 |
-
|
| 259 |
-
# Two candidates under two weightings, and two objectives that disagree.
|
| 260 |
-
candidates = ((1.2, 0.2), (0.5, 1.1)) # Normalized binding and solubility of two peptides.
|
| 261 |
-
for lambdas in ((0.7, 0.3), (0.3, 0.7)):
|
| 262 |
-
report[f"candidate rewards, lambda={lambdas}"] = [round(scalarize(v, lambdas), 4)
|
| 263 |
-
for v in candidates]
|
| 264 |
-
competing = 0.6 * torch.tensor([1.0, 0.8]) + 0.4 * torch.tensor([0.5, -1.2])
|
| 265 |
-
report["competing gradients combined"] = [round(float(v), 4) for v in competing] # (0.8, 0).
|
| 266 |
-
rewards = torch.tensor([1.2, 0.8, 0.3])
|
| 267 |
-
report["selection weights at eta=2"] = [round(float(p), 4)
|
| 268 |
-
for p in (2.0 * rewards).softmax(0)]
|
| 269 |
-
report["schedule value at t=0.6"] = schedule_value(0.6) # 0.96.
|
| 270 |
-
|
| 271 |
-
if not args.quiet:
|
| 272 |
-
print(f"device={device} points={args.samples} steps={args.steps} "
|
| 273 |
-
f"target mode={GAUSSIAN_CENTERS[TARGET_LABEL].tolist()}")
|
| 274 |
-
print_report("Guidance on the four-Gaussian target", report)
|
| 275 |
-
save_run(args.out, config=vars(args), report=report)
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
# %% 08. Run the script
|
| 279 |
-
if __name__ == "__main__":
|
| 280 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_3/latent_diffusion.py
DELETED
|
@@ -1,309 +0,0 @@
|
|
| 1 |
-
"""AMP-Diffusion, the same chain run in the latent space of a protein language model.
|
| 2 |
-
|
| 3 |
-
Gaussian corruption needs a state that can be added to, and an amino acid sequence has no such
|
| 4 |
-
structure. A frozen encoder supplies one. The peptide becomes a tensor of residue embeddings, the
|
| 5 |
-
diffusion runs there, and the language-model head maps the generated latent back to residues.
|
| 6 |
-
Against the image model the state is the latent, a transformer replaces the U-Net, the network
|
| 7 |
-
predicts the clean latent in place of the noise, and a mask marks which positions hold real
|
| 8 |
-
residues.
|
| 9 |
-
|
| 10 |
-
Chapter 3 runs its diffusion clock from the data, so step k = 0 holds a clean latent and
|
| 11 |
-
corruption grows with k, up to pure noise at k = K.
|
| 12 |
-
|
| 13 |
-
Chapter 3, Sections 3.21, 3.29 and 3.46 of the notes. Trained on toy embeddings by default, and
|
| 14 |
-
on ESM-2 embeddings of the course peptides with --dataset esm2. Prints the corrupted coordinate at
|
| 15 |
-
k = 500, one reverse step there, the masked loss, and the polar fraction before and after the
|
| 16 |
-
constraint.
|
| 17 |
-
|
| 18 |
-
python lecture_3/latent_diffusion.py # 800 steps on offline embeddings
|
| 19 |
-
python lecture_3/latent_diffusion.py --steps 20 # a few seconds, for a first look
|
| 20 |
-
python lecture_3/latent_diffusion.py --dataset esm2 # real ESM-2 latents and its head
|
| 21 |
-
"""
|
| 22 |
-
|
| 23 |
-
# %% 01. Imports and configuration
|
| 24 |
-
import math
|
| 25 |
-
|
| 26 |
-
import torch
|
| 27 |
-
import torch.nn as nn
|
| 28 |
-
|
| 29 |
-
from cis6270.nets import count_parameters
|
| 30 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed, train
|
| 31 |
-
|
| 32 |
-
MODEL_NAME = "facebook/esm2_t33_650M_UR50D" # Pretrained ESM-2 encoder.
|
| 33 |
-
EMBED_DIM = 1280 # ESM-2 residue embedding width D.
|
| 34 |
-
MAX_LEN = 32 # Token positions L per padded peptide.
|
| 35 |
-
NUM_LAYERS = 6 # Transformer layers in the denoiser.
|
| 36 |
-
NUM_STEPS = 1000 # Diffusion steps K.
|
| 37 |
-
AMINO_ACIDS = "ACDEFGHIKLMNPQRSTVWY" # The 20 standard amino acids.
|
| 38 |
-
POLAR_RESIDUES = set("STNQYCHKRDE") # Polar or charged side chains.
|
| 39 |
-
POLAR_COLUMNS = [AMINO_ACIDS.index(a) for a in "STNQYCHKRDE"]
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
# %% 02. The schedule tensors
|
| 43 |
-
#
|
| 44 |
-
# The image tensors of Section 3.28, rebuilt here so the file stands alone. Index k holds the
|
| 45 |
-
# quantity at step k, with a zero prefixed so that abar_0 = 1 at the clean latent.
|
| 46 |
-
|
| 47 |
-
betas = torch.cat([torch.zeros(1), torch.linspace(1e-4, 0.02, NUM_STEPS)]) # beta_k.
|
| 48 |
-
alphas = 1.0 - betas # One-step retention alpha_k.
|
| 49 |
-
alphas_cumprod = torch.cumprod(alphas, dim=0) # Cumulative retention abar_k.
|
| 50 |
-
alphas_cumprod_prev = torch.cat([torch.ones(1), alphas_cumprod[:-1]]) # abar_{k-1}.
|
| 51 |
-
posterior_variance = betas * (1.0 - alphas_cumprod_prev) / (1.0 - alphas_cumprod) # beta-tilde_k.
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
# %% 03. Masked corruption
|
| 55 |
-
#
|
| 56 |
-
# Z_k = (sqrt(abar_k) Z_0 + sqrt(1 - abar_k) eps) * M. The reshape to [B, 1, 1] broadcasts one
|
| 57 |
-
# coefficient per sequence across all L positions and all D coordinates, and the mask holds padded
|
| 58 |
-
# positions at exactly zero so the denoiser spends no capacity on them.
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def q_sample_latent(z0, k, mask, noise=None):
|
| 62 |
-
if noise is None:
|
| 63 |
-
noise = torch.randn_like(z0) # [B, L, D] standard Gaussian.
|
| 64 |
-
abar = alphas_cumprod.to(z0.device)[k].reshape(-1, 1, 1)
|
| 65 |
-
z_k = abar.sqrt() * z0 + (1.0 - abar).sqrt() * noise
|
| 66 |
-
return z_k * mask.unsqueeze(-1), noise # Padding stays exactly zero.
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
def timestep_embedding(k, dim):
|
| 70 |
-
half = dim // 2 # Half the coordinates sine, half cosine.
|
| 71 |
-
freqs = torch.exp(-math.log(10000.0)
|
| 72 |
-
* torch.arange(half, device=k.device).to(torch.get_default_dtype())
|
| 73 |
-
/ half)
|
| 74 |
-
args = k.to(torch.get_default_dtype())[:, None] * freqs[None, :] # [B, half].
|
| 75 |
-
return torch.cat([torch.sin(args), torch.cos(args)], dim=1) # [B, dim].
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
# %% 04. The latent denoiser and its loss
|
| 79 |
-
#
|
| 80 |
-
# The step embedding, a learned position embedding and the latent are added before the transformer
|
| 81 |
-
# acts on them, so the step reaches every residue as one shared offset and the attention layers mix
|
| 82 |
-
# the positions. The loss counts valid residues only.
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
class LatentDenoiser(nn.Module):
|
| 86 |
-
def __init__(self, embed_dim=EMBED_DIM, num_layers=NUM_LAYERS,
|
| 87 |
-
num_heads=8, max_len=MAX_LEN):
|
| 88 |
-
super().__init__()
|
| 89 |
-
self.embed_dim = embed_dim
|
| 90 |
-
self.time_mlp = nn.Sequential( # Same sinusoidal step embedding.
|
| 91 |
-
nn.Linear(embed_dim, embed_dim),
|
| 92 |
-
nn.SiLU(),
|
| 93 |
-
nn.Linear(embed_dim, embed_dim),
|
| 94 |
-
)
|
| 95 |
-
self.pos_embed = nn.Embedding(max_len, embed_dim) # Residue index.
|
| 96 |
-
encoder_layer = nn.TransformerEncoderLayer(
|
| 97 |
-
d_model=embed_dim, nhead=num_heads,
|
| 98 |
-
dim_feedforward=4 * embed_dim, batch_first=True,
|
| 99 |
-
)
|
| 100 |
-
self.transformer = nn.TransformerEncoder(
|
| 101 |
-
encoder_layer, num_layers=num_layers,
|
| 102 |
-
enable_nested_tensor=False, # Keep a padded batch on the ordinary attention path.
|
| 103 |
-
)
|
| 104 |
-
self.out = nn.Sequential(nn.LayerNorm(embed_dim),
|
| 105 |
-
nn.Linear(embed_dim, embed_dim))
|
| 106 |
-
|
| 107 |
-
def forward(self, z_k, k, attention_mask):
|
| 108 |
-
t_emb = self.time_mlp(timestep_embedding(k, self.embed_dim))
|
| 109 |
-
positions = torch.arange(z_k.shape[1], device=z_k.device)
|
| 110 |
-
h = (z_k
|
| 111 |
-
+ t_emb.unsqueeze(1) # Shared across positions.
|
| 112 |
-
+ self.pos_embed(positions).unsqueeze(0)) # Shared across batch.
|
| 113 |
-
h = self.transformer(h, src_key_padding_mask=~attention_mask.bool())
|
| 114 |
-
return self.out(h) # Predicted clean latent Z0-hat.
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
def latent_loss(z0_hat, z0, mask):
|
| 118 |
-
error = (z0_hat - z0) ** 2 # [B, L, D] squared error.
|
| 119 |
-
error = error * mask.unsqueeze(-1) # Zero out padded positions.
|
| 120 |
-
return error.sum() / (mask.sum() * z0.shape[-1]) # Valid tokens only.
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
# %% 05. Reverse latent sampling
|
| 124 |
-
#
|
| 125 |
-
# The network predicts the clean latent, so the noise it implies is recovered by rearranging the
|
| 126 |
-
# corruption equation, and the reverse mean is then the image sampler's expression unchanged.
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
@torch.no_grad()
|
| 130 |
-
def p_sample_latent(model, z_k, k, mask):
|
| 131 |
-
z0_hat = model(z_k, k, mask) # Predict the clean latent.
|
| 132 |
-
abar = alphas_cumprod.to(z_k.device)[k].reshape(-1, 1, 1)
|
| 133 |
-
eps_hat = (z_k - abar.sqrt() * z0_hat) / (1.0 - abar).sqrt()
|
| 134 |
-
beta_k = betas.to(z_k.device)[k].reshape(-1, 1, 1)
|
| 135 |
-
alpha_k = alphas.to(z_k.device)[k].reshape(-1, 1, 1)
|
| 136 |
-
mean = (z_k - beta_k / (1.0 - abar).sqrt() * eps_hat) / alpha_k.sqrt()
|
| 137 |
-
var = posterior_variance.to(z_k.device)[k].reshape(-1, 1, 1)
|
| 138 |
-
xi = torch.randn_like(z_k)
|
| 139 |
-
keep = (k > 1).to(torch.get_default_dtype()).reshape(-1, 1, 1)
|
| 140 |
-
return (mean + keep * var.sqrt() * xi) * mask.unsqueeze(-1)
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
@torch.no_grad()
|
| 144 |
-
def sample_latents(model, num_sequences, length, width, device):
|
| 145 |
-
mask = torch.ones(num_sequences, length, device=device)
|
| 146 |
-
z = torch.randn(num_sequences, length, width, device=device)
|
| 147 |
-
for step in range(NUM_STEPS, 0, -1): # Z_K down to Z_0.
|
| 148 |
-
k = torch.full((num_sequences,), step, device=device,
|
| 149 |
-
dtype=torch.long)
|
| 150 |
-
z = p_sample_latent(model, z, k, mask)
|
| 151 |
-
return z
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
# %% 06. Decoding back to amino acids
|
| 155 |
-
#
|
| 156 |
-
# The generated latent is unstandardized first, then read through the head that was trained to
|
| 157 |
-
# score residues from exactly these embeddings. The head also scores special tokens and ambiguity
|
| 158 |
-
# codes, so the columns are restricted to the twenty canonical amino acids before the argmax.
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
@torch.no_grad()
|
| 162 |
-
def decode(z, head, columns, z_mean, z_std):
|
| 163 |
-
latent = z * z_std + z_mean # Undo normalization.
|
| 164 |
-
logits = head(latent) # Full vocabulary logits.
|
| 165 |
-
logits = logits.index_select(-1, columns) # 20 canonical residues.
|
| 166 |
-
scores, choices = logits.max(dim=-1) # Argmax at each position.
|
| 167 |
-
return logits, choices
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
def composition_proxies(seq):
|
| 171 |
-
"""Net charge per residue and polar/charged fraction."""
|
| 172 |
-
L = max(len(seq), 1)
|
| 173 |
-
charge = (seq.count("K") + seq.count("R")
|
| 174 |
-
- seq.count("D") - seq.count("E")) / L
|
| 175 |
-
polar = sum(1 for a in seq if a in POLAR_RESIDUES) / L
|
| 176 |
-
return charge, polar
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
# %% 07. The composition constraint at decoding
|
| 180 |
-
#
|
| 181 |
-
# Guidance makes a high polar fraction more likely without guaranteeing it. The requirement is
|
| 182 |
-
# imposed afterwards by substituting residues at the positions where substitution costs the
|
| 183 |
-
# least logit. One projection at the end satisfies the requirement exactly, which a penalty
|
| 184 |
-
# added to the reward along the way does not.
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def enforce_polar(logits, choices, minimum):
|
| 188 |
-
cols = torch.tensor(POLAR_COLUMNS, device=logits.device)
|
| 189 |
-
polar_scores, pick = logits.index_select(-1, cols).max(dim=-1)
|
| 190 |
-
polar_choices = cols[pick] # best polar residue
|
| 191 |
-
already = torch.isin(choices, cols) # positions already polar
|
| 192 |
-
sequences = []
|
| 193 |
-
for b in range(choices.shape[0]):
|
| 194 |
-
row = choices[b].clone()
|
| 195 |
-
missing = minimum - int(already[b].sum())
|
| 196 |
-
if missing > 0:
|
| 197 |
-
chosen = logits[b].gather(1, row[:, None]).squeeze(1)
|
| 198 |
-
cost = chosen - polar_scores[b] # logit lost by swapping
|
| 199 |
-
cost = cost.masked_fill(already[b], float("inf"))
|
| 200 |
-
_, swap = cost.topk(missing, largest=False)
|
| 201 |
-
row[swap] = polar_choices[b][swap]
|
| 202 |
-
sequences.append("".join(AMINO_ACIDS[i] for i in row.tolist()))
|
| 203 |
-
return sequences
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
# %% 08. The latents, standardized
|
| 207 |
-
#
|
| 208 |
-
# ESM-2 coordinates have means and scales of their own, and a corruption that drove such a cloud
|
| 209 |
-
# toward N(0, I) would have to destroy a systematic offset as well as the structure. Each
|
| 210 |
-
# coordinate is therefore centered and scaled over the valid residues before any diffusion runs,
|
| 211 |
-
# and the two statistics are kept for decoding.
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
def load_latents(dataset: str, device, seed: int):
|
| 215 |
-
"""Return the standardized latents, the mask, the decoding head and its residue columns."""
|
| 216 |
-
if dataset == "toy":
|
| 217 |
-
from cis6270.data import standardize, toy_embeddings
|
| 218 |
-
|
| 219 |
-
z0, mask = toy_embeddings(num_sequences=128, length=16, width=32, seed=seed)
|
| 220 |
-
z0, z_mean, z_std = standardize(z0, mask)
|
| 221 |
-
# Without ESM-2 there is no language-model head, so a fixed projection stands in. It makes
|
| 222 |
-
# the decoding path and the constraint runnable offline; its residues carry no biology.
|
| 223 |
-
generator = torch.Generator().manual_seed(seed)
|
| 224 |
-
weight = torch.randn(len(AMINO_ACIDS), z0.shape[-1], generator=generator) / 4.0
|
| 225 |
-
head = lambda latent: latent @ weight.t().to(latent) # noqa: E731
|
| 226 |
-
columns = torch.arange(len(AMINO_ACIDS))
|
| 227 |
-
else:
|
| 228 |
-
from cis6270.data import esm2_embeddings, standardize
|
| 229 |
-
|
| 230 |
-
z0, mask, tokenizer, head = esm2_embeddings(
|
| 231 |
-
model_name=MODEL_NAME, max_length=MAX_LEN, device=device,
|
| 232 |
-
) # The head is ESM-2's masked language-model head, frozen with the encoder.
|
| 233 |
-
z0, z_mean, z_std = standardize(z0, mask)
|
| 234 |
-
columns = torch.tensor([tokenizer.convert_tokens_to_ids(a) for a in AMINO_ACIDS])
|
| 235 |
-
return z0.to(device), mask.to(device), z_mean.to(device), z_std.to(device), head, columns
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
# %% 09. The report
|
| 239 |
-
def main() -> None:
|
| 240 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 241 |
-
parser.add_argument("--dataset", choices=["toy", "esm2"], default="toy")
|
| 242 |
-
parser.add_argument("--minimum-polar", type=int, default=8, help="polar residues required")
|
| 243 |
-
parser.set_defaults(steps=800, batch_size=16, lr=1e-3, samples=8)
|
| 244 |
-
args = parser.parse_args()
|
| 245 |
-
set_seed(args.seed)
|
| 246 |
-
device = pick_device(args.device)
|
| 247 |
-
|
| 248 |
-
z0_all, mask_all, z_mean, z_std, head, columns = load_latents(args.dataset, device, args.seed)
|
| 249 |
-
num_sequences, length, width = z0_all.shape # [N, L, D].
|
| 250 |
-
denoiser = LatentDenoiser(embed_dim=width, num_layers=NUM_LAYERS, num_heads=8,
|
| 251 |
-
max_len=length).to(device)
|
| 252 |
-
if not args.quiet:
|
| 253 |
-
print(f"device={device} dataset={args.dataset} latents={tuple(z0_all.shape)} "
|
| 254 |
-
f"parameters={count_parameters(denoiser):,}")
|
| 255 |
-
|
| 256 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 257 |
-
rows = torch.randint(0, num_sequences, (args.batch_size,), device=device) # [B].
|
| 258 |
-
z0, mask = z0_all[rows], mask_all[rows] # [B, L, D] and [B, L].
|
| 259 |
-
k = torch.randint(1, NUM_STEPS + 1, (z0.shape[0],), device=device) # One step per item.
|
| 260 |
-
z_k, _ = q_sample_latent(z0, k, mask) # Corrupt the clean latent.
|
| 261 |
-
return latent_loss(denoiser(z_k, k, mask), z0, mask) # Valid residues only.
|
| 262 |
-
|
| 263 |
-
losses = train(denoiser, loss_fn, args.steps, args.lr, quiet=args.quiet)
|
| 264 |
-
generated = sample_latents(denoiser, args.samples, length, width, device) # [S, L, D].
|
| 265 |
-
logits, choices = decode(generated, head, columns.to(device), z_mean, z_std)
|
| 266 |
-
# The argmax decoding before the constraint, and the same sequences after it.
|
| 267 |
-
plain = ["".join(AMINO_ACIDS[int(i)] for i in row) for row in choices]
|
| 268 |
-
constrained = enforce_polar(logits, choices, args.minimum_polar)
|
| 269 |
-
|
| 270 |
-
# The chapter's coordinate, corrupted at k = 500 and carried back one reverse step.
|
| 271 |
-
abar = float(alphas_cumprod[500])
|
| 272 |
-
a_500, sigma_500 = math.sqrt(abar), math.sqrt(1.0 - abar) # 0.2803 and 0.9599.
|
| 273 |
-
z_500 = a_500 * 1.2 + sigma_500 * 0.4 # 0.7204.
|
| 274 |
-
eps_hat = (z_500 - a_500 * 1.05) / sigma_500 # 0.4438 from the prediction z0_hat = 1.05.
|
| 275 |
-
mean_500 = (z_500 - float(betas[500]) / sigma_500 * eps_hat) / math.sqrt(float(alphas[500]))
|
| 276 |
-
# The nine substitution costs of the notes, with two positions still to fill.
|
| 277 |
-
costs = torch.tensor([0.9, 1.4, 2.6, 3.1, 3.3, 4.0, 4.2, 5.5, 6.1])
|
| 278 |
-
|
| 279 |
-
window = min(50, args.steps) # A short run averages over everything it has.
|
| 280 |
-
report = {
|
| 281 |
-
"parameters": count_parameters(denoiser),
|
| 282 |
-
f"loss, first {window} steps": sum(losses[:window]) / window,
|
| 283 |
-
f"loss, last {window} steps": sum(losses[-window:]) / window,
|
| 284 |
-
"valid residues per batch": int(mask_all[:args.batch_size].sum()),
|
| 285 |
-
"a_500 and sigma_500": [round(a_500, 4), round(sigma_500, 4)], # 0.2803 and 0.9599.
|
| 286 |
-
"z_500 from z_0=1.2, eps=0.4": z_500, # 0.7204.
|
| 287 |
-
"eps implied by z0_hat=1.05": eps_hat, # 0.4438, against the draw 0.4.
|
| 288 |
-
"reverse mean at k=500": mean_500, # 0.7193.
|
| 289 |
-
"reverse standard deviation": math.sqrt(float(posterior_variance[500])), # 0.1002.
|
| 290 |
-
"clean estimate at a=0.8 from eps=-0.8": (7.4 + 0.6 * 0.8) / 0.8, # 9.85.
|
| 291 |
-
"noise recovered from it": (7.4 - 0.8 * 9.85) / 0.6, # -0.8.
|
| 292 |
-
"polar fraction, plain decoding": sum(
|
| 293 |
-
composition_proxies(s)[1] for s in plain) / len(plain),
|
| 294 |
-
"polar fraction, constrained": sum(
|
| 295 |
-
composition_proxies(s)[1] for s in constrained) / len(constrained),
|
| 296 |
-
"net charge per residue, constrained": sum(
|
| 297 |
-
composition_proxies(s)[0] for s in constrained) / len(constrained),
|
| 298 |
-
"two cheapest substitutions": [round(float(v), 1)
|
| 299 |
-
for v in costs.topk(2, largest=False)[0]],
|
| 300 |
-
"logit surrendered": float(costs.topk(2, largest=False)[0].sum()), # 2.3.
|
| 301 |
-
}
|
| 302 |
-
print_report(f"AMP-Diffusion on {args.dataset} latents", report, constrained)
|
| 303 |
-
save_run(args.out, model=denoiser, config=vars(args), report=report, losses=losses,
|
| 304 |
-
samples=constrained)
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
# %% 10. Run the script
|
| 308 |
-
if __name__ == "__main__":
|
| 309 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_3/samplers.py
DELETED
|
@@ -1,247 +0,0 @@
|
|
| 1 |
-
"""Three samplers reading one score.
|
| 2 |
-
|
| 3 |
-
Once a score is available, generation can take three forms. The reverse-time equation discretized
|
| 4 |
-
on a grid gives ancestral DDPM sampling, which injects fresh noise at every step. Rewriting the
|
| 5 |
-
Laplacian of the Fokker-Planck equation as a divergence gives the probability-flow ODE, which
|
| 6 |
-
reproduces the same marginals with no noise at all. Holding one density fixed and running its
|
| 7 |
-
score as a drift gives Langevin dynamics, which needs a ladder of noise levels to cross between
|
| 8 |
-
modes. The target here is the four-Gaussian mixture, whose corrupted score is a closed form, so
|
| 9 |
-
all three run on the same exact field and nothing is trained.
|
| 10 |
-
|
| 11 |
-
Chapter 3 runs its diffusion clock from the data, so t = 0 holds a clean sample and corruption
|
| 12 |
-
grows with t. Generation traverses the same interval from t = 1 back to t = 0.
|
| 13 |
-
|
| 14 |
-
Chapter 3, Sections 3.22, 3.26, 3.32, 3.34 and 3.35 of the notes. Prints the mode coverage of each
|
| 15 |
-
sampler, the number of score evaluations each spends, and the chapter's one backward step from
|
| 16 |
-
x = 1.2 under both dynamics.
|
| 17 |
-
|
| 18 |
-
python lecture_3/samplers.py # 200 steps for each of the three
|
| 19 |
-
python lecture_3/samplers.py --steps 20 # a coarse grid, for a first look
|
| 20 |
-
"""
|
| 21 |
-
|
| 22 |
-
# %% 01. Imports and configuration
|
| 23 |
-
import math
|
| 24 |
-
|
| 25 |
-
import torch
|
| 26 |
-
|
| 27 |
-
from cis6270.data import GAUSSIAN_CENTERS, GAUSSIAN_STD
|
| 28 |
-
from cis6270.metrics import mean_log_density, mode_report
|
| 29 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed
|
| 30 |
-
|
| 31 |
-
BETA_MIN = 0.1 # beta(0) of the linear variance-preserving schedule.
|
| 32 |
-
BETA_MAX = 20.0 # beta(1) of the same schedule.
|
| 33 |
-
LADDER_START = 0.7 # Noisiest level of the Langevin ladder, in diffusion time.
|
| 34 |
-
LADDER_END = 0.01 # Least noisy level of the ladder.
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
# %% 02. The target and its exact score at any noise level
|
| 38 |
-
#
|
| 39 |
-
# Corrupting a Gaussian mixture by X_t = a_t X_0 + sigma_t eps gives a Gaussian mixture with means
|
| 40 |
-
# a_t mu_m and the common variance a_t^2 s^2 + sigma_t^2, so the score of p_t is a closed form at
|
| 41 |
-
# every level. Every call is counted, because the cost of a sampler is the number of evaluations
|
| 42 |
-
# a trained network would have served.
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def beta(t: float) -> float:
|
| 46 |
-
"""The noising rate beta(t) = 0.1 + 19.9 t of the linear schedule."""
|
| 47 |
-
return BETA_MIN + (BETA_MAX - BETA_MIN) * t
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def signal(t: float) -> float:
|
| 51 |
-
"""a_t = exp(-B(t)/2) with B(t) = 0.1 t + 9.95 t^2, the accumulated noising."""
|
| 52 |
-
return math.exp(-(BETA_MIN * t + 0.5 * (BETA_MAX - BETA_MIN) * t ** 2) / 2.0)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
class ExactScore:
|
| 56 |
-
"""The score of p_t at a given signal coefficient, with a counter on the calls."""
|
| 57 |
-
|
| 58 |
-
def __init__(self):
|
| 59 |
-
self.calls = 0 # One call stands for one network evaluation.
|
| 60 |
-
|
| 61 |
-
def __call__(self, x: torch.Tensor, a_t: float) -> torch.Tensor:
|
| 62 |
-
"""x: [B, 2], a_t the signal coefficient -> [B, 2]."""
|
| 63 |
-
self.calls += 1
|
| 64 |
-
variance = a_t ** 2 * GAUSSIAN_STD ** 2 + (1.0 - a_t ** 2) # Width of p_t.
|
| 65 |
-
offset = x[:, None, :] - a_t * GAUSSIAN_CENTERS.to(x)[None, :, :] # [B, 4, 2].
|
| 66 |
-
log_weight = -(offset ** 2).sum(dim=2) / (2.0 * variance) # [B, 4] equal mixture weights.
|
| 67 |
-
responsibility = log_weight.softmax(dim=1)[:, :, None] # [B, 4, 1].
|
| 68 |
-
return (responsibility * (-offset / variance)).sum(dim=1) # [B, 2].
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def make_schedule(num_steps: int) -> dict:
|
| 72 |
-
"""The DDPM tensors on a grid of K steps, read off the continuous schedule.
|
| 73 |
-
|
| 74 |
-
Setting abar_k = a_{t_k}^2 at t_k = k/K and recovering alpha_k = abar_k / abar_{k-1} matches
|
| 75 |
-
the continuous marginals at every grid point. At K = 1000 the betas agree with the linear
|
| 76 |
-
sequence from 1e-4 to 0.02 to the accuracy of log(1 - beta) = -beta, and at a coarse K every
|
| 77 |
-
beta_k stays below one, which beta(t_k)/K does not.
|
| 78 |
-
"""
|
| 79 |
-
grid = (torch.arange(0, num_steps + 1).to(torch.get_default_dtype())
|
| 80 |
-
/ num_steps) # [K+1] the times t_k, with t_0 = 0.
|
| 81 |
-
alphas_cumprod = torch.exp(
|
| 82 |
-
-(BETA_MIN * grid + 0.5 * (BETA_MAX - BETA_MIN) * grid ** 2)
|
| 83 |
-
) # [K+1] abar_k, with abar_0 = 1 at the clean state.
|
| 84 |
-
alphas_cumprod_prev = torch.cat([torch.ones(1), alphas_cumprod[:-1]]) # abar_{k-1}.
|
| 85 |
-
alphas = alphas_cumprod / alphas_cumprod_prev # One-step retention alpha_k.
|
| 86 |
-
betas = 1.0 - alphas # Per-step variance beta_k, zero at the placeholder index 0.
|
| 87 |
-
return {
|
| 88 |
-
"betas": betas, "alphas": alphas, "alphas_cumprod": alphas_cumprod,
|
| 89 |
-
"posterior_variance": betas * (1.0 - alphas_cumprod_prev) / (1.0 - alphas_cumprod),
|
| 90 |
-
}
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
# %% 03. Ancestral DDPM sampling
|
| 94 |
-
#
|
| 95 |
-
# x_{k-1} = mu_theta(x_k, k) + sqrt(beta-tilde_k) z with the reverse mean built from the noise
|
| 96 |
-
# prediction, eps = -sigma_k s_k. Fresh noise enters at every step except the last.
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
@torch.no_grad()
|
| 100 |
-
def ancestral_sample(score, schedule, num_points: int, generator=None) -> torch.Tensor:
|
| 101 |
-
"""Run the reverse chain from X_K ~ N(0, I) down to X_0. Returns [N, 2]."""
|
| 102 |
-
num_steps = len(schedule["betas"]) - 1
|
| 103 |
-
x = torch.randn(num_points, 2, generator=generator) # [N, 2] the terminal Gaussian.
|
| 104 |
-
for step in range(num_steps, 0, -1): # k = K down to k = 1.
|
| 105 |
-
abar_k = float(schedule["alphas_cumprod"][step])
|
| 106 |
-
sigma_k = math.sqrt(1.0 - abar_k) # Corruption standard deviation at this step.
|
| 107 |
-
eps_theta = -sigma_k * score(x, math.sqrt(abar_k)) # [N, 2] the noise the score implies.
|
| 108 |
-
mean = (x - float(schedule["betas"][step]) / sigma_k * eps_theta) / math.sqrt(
|
| 109 |
-
float(schedule["alphas"][step])
|
| 110 |
-
) # Reverse mean mu_theta.
|
| 111 |
-
if step > 1: # No noise at the final step.
|
| 112 |
-
variance = float(schedule["posterior_variance"][step])
|
| 113 |
-
x = mean + math.sqrt(variance) * torch.randn(x.shape, generator=generator)
|
| 114 |
-
else:
|
| 115 |
-
x = mean
|
| 116 |
-
return x # [N, 2].
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
# %% 04. The probability-flow ODE
|
| 120 |
-
#
|
| 121 |
-
# v_t(x) = -(1/2) beta(t) [x + s_t(x)] has the same marginals with no Brownian increment, and
|
| 122 |
-
# generation integrates it backward, x_{t - dt} = x_t - dt v_t(x_t). Once X_T is drawn the whole
|
| 123 |
-
# trajectory is fixed.
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
@torch.no_grad()
|
| 127 |
-
def probability_flow_sample(score, num_points: int, steps: int, generator=None) -> torch.Tensor:
|
| 128 |
-
"""Euler integration of the probability-flow ODE from t = 1 to t = 0. Returns [N, 2]."""
|
| 129 |
-
x = torch.randn(num_points, 2, generator=generator) # [N, 2] the terminal Gaussian.
|
| 130 |
-
dt = 1.0 / steps # Positive step, taken in the direction of decreasing t.
|
| 131 |
-
for step in range(steps, 0, -1):
|
| 132 |
-
t = step * dt
|
| 133 |
-
a_t = signal(t)
|
| 134 |
-
velocity = -0.5 * beta(t) * (x + score(x, a_t)) # [N, 2] forward-time velocity.
|
| 135 |
-
x = x - dt * velocity # One backward Euler step.
|
| 136 |
-
return x # [N, 2].
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
# %% 05. Langevin dynamics and the noise ladder
|
| 140 |
-
#
|
| 141 |
-
# dX = s(X) d_tau + sqrt(2) dW holds p stationary at one fixed density, and the Euler-Maruyama
|
| 142 |
-
# update is x' = x + eta s(x) + sqrt(2 eta) z. A chain run at the data density alone crosses
|
| 143 |
-
# between modes slowly, so the levels are visited in decreasing order of corruption, with a few
|
| 144 |
-
# iterations at each.
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
@torch.no_grad()
|
| 148 |
-
def annealed_langevin_sample(score, num_points, levels, per_level, scale, generator=None):
|
| 149 |
-
"""Run ``per_level`` Langevin iterations at each level of the ladder. Returns [N, 2]."""
|
| 150 |
-
x = torch.randn(num_points, 2, generator=generator) # [N, 2] a Gaussian start.
|
| 151 |
-
for t in levels:
|
| 152 |
-
a_t = signal(t)
|
| 153 |
-
width = a_t ** 2 * GAUSSIAN_STD ** 2 + (1.0 - a_t ** 2) # Width of p_t at this level.
|
| 154 |
-
eta = scale * width # Step size tied to the level, large when the density is broad.
|
| 155 |
-
for _ in range(per_level):
|
| 156 |
-
noise = torch.randn(x.shape, generator=generator) # [N, 2] fresh at every iteration.
|
| 157 |
-
x = x + eta * score(x, a_t) + math.sqrt(2.0 * eta) * noise
|
| 158 |
-
return x # [N, 2] at the least corrupted level of the ladder.
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
def langevin_step(x: float, score_value: float, eta: float, z: float) -> float:
|
| 162 |
-
"""One Langevin update in one coordinate, the chapter's x + eta s(x) + sqrt(2 eta) z."""
|
| 163 |
-
return x + eta * score_value + math.sqrt(2.0 * eta) * z
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
# %% 06. One backward step under both dynamics
|
| 167 |
-
#
|
| 168 |
-
# The score appears in the reverse stochastic equation with the full g(t)^2 and in the
|
| 169 |
-
# deterministic velocity with half of it, which is the only structural difference between the
|
| 170 |
-
# two samplers.
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
def one_step_comparison(x_t, beta_t, sigma_t, eps_theta, dt) -> dict:
|
| 174 |
-
"""The chapter's state carried one step by the ODE and by the reverse-time equation."""
|
| 175 |
-
score_value = -eps_theta / sigma_t # The score the noise prediction implies.
|
| 176 |
-
velocity = -0.5 * beta_t * (x_t + score_value) # Probability-flow velocity.
|
| 177 |
-
reverse_drift = -0.5 * beta_t * x_t - beta_t * score_value # f - g^2 s.
|
| 178 |
-
return {
|
| 179 |
-
"score": score_value, "velocity": velocity, "ode_next": x_t - dt * velocity,
|
| 180 |
-
"sde_drift": reverse_drift, "sde_mean": x_t - dt * reverse_drift,
|
| 181 |
-
"sde_noise_std": math.sqrt(beta_t * dt),
|
| 182 |
-
}
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
# %% 07. The report
|
| 186 |
-
def main() -> None:
|
| 187 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 188 |
-
parser.add_argument("--levels", type=int, default=20, help="rungs of the Langevin ladder")
|
| 189 |
-
parser.add_argument("--langevin-scale", type=float, default=0.15, help="step size over width")
|
| 190 |
-
parser.set_defaults(steps=200, samples=2048)
|
| 191 |
-
args = parser.parse_args()
|
| 192 |
-
set_seed(args.seed)
|
| 193 |
-
device = pick_device(args.device) # Two coordinates per point; a CPU is enough.
|
| 194 |
-
|
| 195 |
-
results, counts = {}, {}
|
| 196 |
-
generator = torch.Generator().manual_seed(args.seed)
|
| 197 |
-
score = ExactScore()
|
| 198 |
-
results["ancestral DDPM"] = ancestral_sample(
|
| 199 |
-
score, make_schedule(args.steps), args.samples, generator,
|
| 200 |
-
)
|
| 201 |
-
counts["ancestral DDPM"] = score.calls
|
| 202 |
-
|
| 203 |
-
generator = torch.Generator().manual_seed(args.seed) # The same starting noise for each.
|
| 204 |
-
score = ExactScore()
|
| 205 |
-
results["probability flow"] = probability_flow_sample(
|
| 206 |
-
score, args.samples, args.steps, generator,
|
| 207 |
-
)
|
| 208 |
-
counts["probability flow"] = score.calls
|
| 209 |
-
|
| 210 |
-
generator = torch.Generator().manual_seed(args.seed)
|
| 211 |
-
score = ExactScore()
|
| 212 |
-
per_level = max(args.steps // args.levels, 1) # Spend the same budget as the other two.
|
| 213 |
-
ladder = [LADDER_START + (LADDER_END - LADDER_START) * i / (args.levels - 1)
|
| 214 |
-
for i in range(args.levels)] # Decreasing corruption.
|
| 215 |
-
results["annealed Langevin"] = annealed_langevin_sample(
|
| 216 |
-
score, args.samples, ladder, per_level, args.langevin_scale, generator,
|
| 217 |
-
)
|
| 218 |
-
counts["annealed Langevin"] = score.calls
|
| 219 |
-
|
| 220 |
-
report = {}
|
| 221 |
-
for name, points in results.items():
|
| 222 |
-
measured = mode_report(points)
|
| 223 |
-
report[f"{name}: score evaluations"] = counts[name]
|
| 224 |
-
report[f"{name}: modes found"] = measured["modes_found"]
|
| 225 |
-
report[f"{name}: mode entropy"] = measured["mode_entropy"] # log 4 = 1.386 when even.
|
| 226 |
-
report[f"{name}: mode fractions"] = measured["mode_fractions"]
|
| 227 |
-
report[f"{name}: distance to mode"] = measured["nearest_distance"] # 0.44 on the target.
|
| 228 |
-
report[f"{name}: mean log density"] = mean_log_density(points)
|
| 229 |
-
|
| 230 |
-
step = one_step_comparison(1.2, 2.0, 0.5, 0.8, 0.1)
|
| 231 |
-
report["score at the worked state"] = step["score"] # -1.6.
|
| 232 |
-
report["probability-flow velocity"] = step["velocity"] # 0.4.
|
| 233 |
-
report["ODE next state"] = step["ode_next"] # 1.16.
|
| 234 |
-
report["reverse-time drift"] = step["sde_drift"] # 2.0.
|
| 235 |
-
report["reverse-time mean"] = step["sde_mean"] # 1.0.
|
| 236 |
-
report["reverse-time noise std"] = step["sde_noise_std"] # 0.4472.
|
| 237 |
-
report["Langevin step from x=2"] = langevin_step(2.0, -2.0, 0.01, 0.5) # 2.0507.
|
| 238 |
-
|
| 239 |
-
if not args.quiet:
|
| 240 |
-
print(f"device={device} points={args.samples} steps={args.steps} seed={args.seed}")
|
| 241 |
-
print_report("Three samplers on one exact score", report)
|
| 242 |
-
save_run(args.out, config=vars(args), report=report)
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
# %% 08. Run the script
|
| 246 |
-
if __name__ == "__main__":
|
| 247 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_3/score_matching.py
DELETED
|
@@ -1,302 +0,0 @@
|
|
| 1 |
-
"""Three objectives that fit the same score field.
|
| 2 |
-
|
| 3 |
-
The loss we would like to minimize, the squared distance from a model to the gradient of the log
|
| 4 |
-
data density, holds a target that a sample does not give us. One integration by parts removes that
|
| 5 |
-
target and leaves the squared norm of the model plus its divergence. One Gaussian corruption
|
| 6 |
-
removes it a second way and leaves a plain regression on the noise we drew. Both forms have the
|
| 7 |
-
minimizer of the first, and all three are fitted here on a one-dimensional Gaussian mixture whose
|
| 8 |
-
exact score is a closed form to measure them against.
|
| 9 |
-
|
| 10 |
-
Chapter 3 runs its diffusion clock from the data, so t = 0 holds a clean sample and corruption
|
| 11 |
-
grows with t. The level fitted here is the running one of the chapter, a_t = 0.8 and sigma_t = 0.6.
|
| 12 |
-
|
| 13 |
-
Chapter 3, Sections 3.14 through 3.17 of the notes. Prints the three fitted score errors, the
|
| 14 |
-
integration-by-parts identity, the minimizer of each objective, the Hutchinson trace estimate, and
|
| 15 |
-
the posterior average at x_t = 7.4.
|
| 16 |
-
|
| 17 |
-
python lecture_3/score_matching.py # fit all three objectives
|
| 18 |
-
python lecture_3/score_matching.py --steps 20 # a few seconds, for a first look
|
| 19 |
-
"""
|
| 20 |
-
|
| 21 |
-
# %% 01. Imports and configuration
|
| 22 |
-
import math
|
| 23 |
-
|
| 24 |
-
import torch
|
| 25 |
-
|
| 26 |
-
from cis6270.nets import VelocityMLP, count_parameters
|
| 27 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed, train
|
| 28 |
-
|
| 29 |
-
A_T = 0.8 # Signal coefficient of the level fitted here.
|
| 30 |
-
SIGMA_T = 0.6 # Noise standard deviation at the same level, with a_t^2 + sigma_t^2 = 1.
|
| 31 |
-
MIXTURE_MEANS = torch.tensor([-2.0, 2.0]) # The two clean modes.
|
| 32 |
-
MIXTURE_WEIGHTS = torch.tensor([0.3, 0.7]) # Their mixture weights.
|
| 33 |
-
MIXTURE_STD = 0.5 # Standard deviation of each clean component.
|
| 34 |
-
TRACE_DIM = 64 # The chapter's 64-dimensional descriptor, used for the trace estimate.
|
| 35 |
-
NAMES = ("explicit", "implicit", "denoising") # The three objectives, in the order derived.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
# %% 02. The corrupted mixture and its exact score
|
| 39 |
-
#
|
| 40 |
-
# A Gaussian mixture corrupted by X_t = a_t X_0 + sigma_t eps is a Gaussian mixture again, with
|
| 41 |
-
# means a_t mu_m and variance a_t^2 s^2 + sigma_t^2, so its score is a closed form.
|
| 42 |
-
|
| 43 |
-
CORRUPTED_MEANS = A_T * MIXTURE_MEANS # [M] means of p_t.
|
| 44 |
-
CORRUPTED_VARIANCE = A_T ** 2 * MIXTURE_STD ** 2 + SIGMA_T ** 2 # Shared variance of p_t.
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def sample_clean(num_points: int, generator=None) -> torch.Tensor:
|
| 48 |
-
"""Draw clean samples from the mixture. Returns [N, 1]."""
|
| 49 |
-
which = torch.multinomial(MIXTURE_WEIGHTS, num_points, True, generator=generator) # [N].
|
| 50 |
-
noise = torch.randn(num_points, generator=generator) * MIXTURE_STD # [N].
|
| 51 |
-
return (MIXTURE_MEANS[which] + noise)[:, None] # [N, 1] clean points.
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def exact_score(x: torch.Tensor) -> torch.Tensor:
|
| 55 |
-
"""The score of the corrupted mixture p_t at each row of x. [N, 1] -> [N, 1]."""
|
| 56 |
-
offset = x - CORRUPTED_MEANS[None, :] # [N, M] displacement from each corrupted mean.
|
| 57 |
-
log_weight = MIXTURE_WEIGHTS.log()[None, :] - offset ** 2 / (2.0 * CORRUPTED_VARIANCE)
|
| 58 |
-
responsibility = log_weight.softmax(dim=1) # [N, M] posterior over the components.
|
| 59 |
-
return (responsibility * (-offset / CORRUPTED_VARIANCE)).sum(dim=1, keepdim=True) # [N, 1].
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def model_score(model, x: torch.Tensor) -> torch.Tensor:
|
| 63 |
-
"""The network's score at x. One level is fitted here, so the time input is held at a_t."""
|
| 64 |
-
return model(x, torch.full((x.shape[0],), A_T)) # [N, 1].
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def divergence(field, x: torch.Tensor) -> torch.Tensor:
|
| 68 |
-
"""The divergence of a field, which in one coordinate is the ordinary derivative. [N, 1]."""
|
| 69 |
-
x = x.requires_grad_(True) # Differentiate with respect to the input, not the weights.
|
| 70 |
-
d_s, = torch.autograd.grad(field(x).sum(), x, create_graph=True) # Trace of the Jacobian.
|
| 71 |
-
return d_s
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
# %% 03. Explicit score matching
|
| 75 |
-
#
|
| 76 |
-
# J(theta) = (1/2) E || s_theta(x) - grad log p(x) ||^2, averaged over samples of p. The target is
|
| 77 |
-
# unavailable in general and available here, so this objective is the reference for the other two.
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def explicit_loss(model, x: torch.Tensor) -> torch.Tensor:
|
| 81 |
-
"""Half the squared distance between the predicted score and the exact one."""
|
| 82 |
-
return 0.5 * (model_score(model, x) - exact_score(x)).square().sum(dim=1).mean()
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
# %% 04. The integration by parts that removes the data score
|
| 86 |
-
#
|
| 87 |
-
# E_p[s_theta . grad log p] = -E_p[div s_theta], from p grad log p = grad p followed by one
|
| 88 |
-
# integration by parts per coordinate, the boundary term vanishing for a density with Gaussian
|
| 89 |
-
# tails.
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def integration_by_parts_gap(field, x: torch.Tensor) -> dict:
|
| 93 |
-
"""The two sides of the identity on one sample, and the gap between them."""
|
| 94 |
-
left = float((field(x) * exact_score(x)).sum(dim=1).mean().detach())
|
| 95 |
-
right = float(-divergence(field, x.clone()).sum(dim=1).mean().detach())
|
| 96 |
-
return {"left": left, "right": right, "gap": abs(left - right)}
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
# %% 05. Implicit score matching with the trace term
|
| 100 |
-
#
|
| 101 |
-
# Substituting the identity leaves J(theta) = E[ (1/2) ||s_theta||^2 + div s_theta ] + C, with C a
|
| 102 |
-
# constant in theta. On the model s_a(x) = -a x with centered data of variance v, the expectation
|
| 103 |
-
# reduces to (1/2) a^2 v - a, whose minimizer is a = 1/v.
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
def implicit_loss(model, x: torch.Tensor) -> torch.Tensor:
|
| 107 |
-
"""The squared norm of the model plus its divergence, with the data score gone."""
|
| 108 |
-
field = lambda z: model_score(model, z) # noqa: E731
|
| 109 |
-
s = field(x) # [N, 1].
|
| 110 |
-
return (0.5 * s.square().sum(dim=1) + divergence(field, x.clone()).sum(dim=1)).mean()
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def one_parameter_loss(a: float, variance: float) -> float:
|
| 114 |
-
"""The implicit loss of s_a(x) = -a x on centered data of the given variance."""
|
| 115 |
-
return 0.5 * a ** 2 * variance - a # Minimized at a = 1 / variance.
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
# %% 06. Denoising score matching
|
| 119 |
-
#
|
| 120 |
-
# For a pair drawn as X_t = a_t X_0 + sigma_t eps the conditional score is known exactly,
|
| 121 |
-
# grad log q_t(x | x_0) = -(x - a_t x_0)/sigma_t^2 = -eps/sigma_t. Its posterior average at a fixed
|
| 122 |
-
# state is the marginal score, which is therefore the minimizer of the regression.
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def denoising_loss(model, x_0: torch.Tensor, epsilon: torch.Tensor) -> torch.Tensor:
|
| 126 |
-
"""Half the squared distance between the predicted score and -eps / sigma_t."""
|
| 127 |
-
x_t = A_T * x_0 + SIGMA_T * epsilon # [N, 1] the corrupted state.
|
| 128 |
-
return 0.5 * (model_score(model, x_t) + epsilon / SIGMA_T).square().sum(dim=1).mean()
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
# %% 07. One minimizer for the three objectives
|
| 132 |
-
#
|
| 133 |
-
# Along the family s_a = a s_exact each objective is a quadratic in a, and its minimizer is that
|
| 134 |
-
# objective's own numerator over the shared denominator E[||s||^2]. The explicit numerator is
|
| 135 |
-
# E[s . grad log p], the implicit one is -E[div s], which the identity of Section 04 makes the
|
| 136 |
-
# same number, and the denoising one is E[s . (-eps/sigma_t)], which is that number again because
|
| 137 |
-
# the posterior average of the conditional score is the marginal score.
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
def minimizers(x_0, epsilon) -> dict:
|
| 141 |
-
"""The minimizing a of each objective along the family a s_exact, on one sample."""
|
| 142 |
-
x_t = A_T * x_0 + SIGMA_T * epsilon # [N, 1] samples of p_t and the draws that made them.
|
| 143 |
-
s = exact_score(x_t) # [N, 1] the exact field of the family.
|
| 144 |
-
squared = float(s.square().sum(dim=1).mean()) # E ||s||^2, the shared denominator.
|
| 145 |
-
data = float((s * exact_score(x_t)).sum(dim=1).mean()) # E[s . grad log p], the explicit one.
|
| 146 |
-
div_s = float(divergence(exact_score, x_t.clone()).detach().sum(dim=1).mean()) # E div s.
|
| 147 |
-
target = float((s * (-epsilon / SIGMA_T)).sum(dim=1).mean()) # E[s . (-eps/sigma_t)].
|
| 148 |
-
return {"explicit": data / squared, "implicit": -div_s / squared,
|
| 149 |
-
"denoising": target / squared}
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
# %% 08. Random directions for the trace
|
| 153 |
-
#
|
| 154 |
-
# Extracting the d diagonal entries of the Jacobian exactly costs d backward passes, 64 for a
|
| 155 |
-
# learned descriptor and 3072 for a colour image on a 32-by-32 grid. Drawing v with E[v v^T] = I
|
| 156 |
-
# and averaging v^T (grad s) v returns the same number at one pass per direction.
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
def high_dimensional_score(x: torch.Tensor, means: torch.Tensor, variance: float) -> torch.Tensor:
|
| 160 |
-
"""The score of a two-component Gaussian mixture in TRACE_DIM coordinates. [N, d] -> [N, d]."""
|
| 161 |
-
offset = x[:, None, :] - means[None, :, :] # [N, 2, d].
|
| 162 |
-
log_weight = -(offset ** 2).sum(dim=2) / (2.0 * variance) # [N, 2] equal mixture weights.
|
| 163 |
-
responsibility = log_weight.softmax(dim=1)[:, :, None] # [N, 2, 1].
|
| 164 |
-
return (responsibility * (-offset / variance)).sum(dim=1) # [N, d].
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
def exact_trace(field, x: torch.Tensor) -> tuple[torch.Tensor, int]:
|
| 168 |
-
"""The divergence coordinate by coordinate, and the number of backward passes it took."""
|
| 169 |
-
x = x.clone().requires_grad_(True)
|
| 170 |
-
s = field(x) # [N, d].
|
| 171 |
-
total = torch.zeros(x.shape[0])
|
| 172 |
-
for i in range(x.shape[1]): # One backward pass per coordinate.
|
| 173 |
-
column, = torch.autograd.grad(s[:, i].sum(), x, retain_graph=True)
|
| 174 |
-
total = total + column[:, i]
|
| 175 |
-
return total.detach(), x.shape[1]
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
def hutchinson_trace(field, x: torch.Tensor, probes: int, generator=None):
|
| 179 |
-
"""Average v^T (grad s) v over ``probes`` random directions, one pass each."""
|
| 180 |
-
# A score field has a symmetric Jacobian, the Hessian of a log density, so the
|
| 181 |
-
# vector-Jacobian product taken here equals the Jacobian-vector product of the notes.
|
| 182 |
-
estimates = []
|
| 183 |
-
for _ in range(probes):
|
| 184 |
-
v = torch.randn(x.shape, generator=generator) # [N, d] direction with E[v v^T] = I.
|
| 185 |
-
point = x.clone().requires_grad_(True)
|
| 186 |
-
projected, = torch.autograd.grad((field(point) * v).sum(), point) # [N, d] one pass.
|
| 187 |
-
estimates.append((v * projected).sum(dim=1).detach()) # [N] unbiased per direction.
|
| 188 |
-
stacked = torch.stack(estimates) # [probes, N].
|
| 189 |
-
return stacked.mean(dim=0), stacked
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
# %% 09. The posterior average at x_t = 7.4
|
| 193 |
-
#
|
| 194 |
-
# The clean value 10 has prior weight 0.3 and the value 9 has 0.7, and at a_t = 0.8 and
|
| 195 |
-
# sigma_t = 0.6 both have conditional means near the observation 7.4.
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
def posterior_report(x_t: float, candidates, priors, epsilon_theta: float) -> dict:
|
| 199 |
-
"""Posterior weights, the clean estimate and the three predictions at one observation."""
|
| 200 |
-
candidates, priors = torch.tensor(candidates), torch.tensor(priors) # [C] and [C].
|
| 201 |
-
conditional_mean = A_T * candidates # [C] corruption mean of each candidate.
|
| 202 |
-
weight = priors * torch.exp(-(x_t - conditional_mean) ** 2 / (2.0 * SIGMA_T ** 2)) # [C].
|
| 203 |
-
posterior = weight / weight.sum() # [C] weights after observing x_t.
|
| 204 |
-
conditional_score = -(x_t - conditional_mean) / SIGMA_T ** 2 # [C] one per candidate.
|
| 205 |
-
marginal = float((posterior * conditional_score).sum()) # The averaged score.
|
| 206 |
-
clean_mean = float((posterior * candidates).sum()) # E[X_0 | X_t = x_t].
|
| 207 |
-
score_hat = -epsilon_theta / SIGMA_T # The score the chapter's prediction implies.
|
| 208 |
-
return {
|
| 209 |
-
"weights": [round(float(w), 4) for w in posterior],
|
| 210 |
-
"clean_mean": clean_mean,
|
| 211 |
-
"conditional_scores": [round(float(s), 4) for s in conditional_score],
|
| 212 |
-
"marginal": [marginal, (A_T * clean_mean - x_t) / SIGMA_T ** 2], # Two routes, one number.
|
| 213 |
-
# The posterior variance of the target, which no function of x_t alone can remove.
|
| 214 |
-
"residual": float((posterior * (marginal - conditional_score) ** 2).sum()),
|
| 215 |
-
"score_hat": score_hat,
|
| 216 |
-
# The clean estimate from the noise, from the score, and from an optimal network.
|
| 217 |
-
"x0": [(x_t - SIGMA_T * epsilon_theta) / A_T, (x_t + SIGMA_T ** 2 * score_hat) / A_T,
|
| 218 |
-
(x_t + SIGMA_T ** 2 * marginal) / A_T],
|
| 219 |
-
}
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
# %% 10. The report
|
| 223 |
-
def main() -> None:
|
| 224 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 225 |
-
parser.add_argument("--probes", type=int, default=32, help="random directions for the trace")
|
| 226 |
-
parser.set_defaults(steps=250, batch_size=256, width=64, lr=1e-2)
|
| 227 |
-
args = parser.parse_args()
|
| 228 |
-
set_seed(args.seed)
|
| 229 |
-
device = pick_device(args.device) # The fields here are one-dimensional; a CPU is enough.
|
| 230 |
-
generator = torch.Generator().manual_seed(args.seed + 1) # Draws for the measurements below.
|
| 231 |
-
|
| 232 |
-
objectives = {}
|
| 233 |
-
for name in NAMES:
|
| 234 |
-
set_seed(args.seed) # Identical initialization for the three fits.
|
| 235 |
-
model = VelocityMLP(dim=1, width=args.width, depth=2)
|
| 236 |
-
# The training batches restart with the model, so the only difference between the three
|
| 237 |
-
# runs is the objective itself.
|
| 238 |
-
batches = torch.Generator().manual_seed(args.seed + 3)
|
| 239 |
-
|
| 240 |
-
def loss_fn(step: int, name=name, model=model, generator=batches) -> torch.Tensor:
|
| 241 |
-
x_0 = sample_clean(args.batch_size, generator) # [B, 1] clean batch.
|
| 242 |
-
epsilon = torch.randn(x_0.shape, generator=generator) # [B, 1] corruption draw.
|
| 243 |
-
if name == "denoising":
|
| 244 |
-
return denoising_loss(model, x_0, epsilon)
|
| 245 |
-
x_t = A_T * x_0 + SIGMA_T * epsilon # [B, 1] samples of p_t for the other two.
|
| 246 |
-
return explicit_loss(model, x_t) if name == "explicit" else implicit_loss(model, x_t)
|
| 247 |
-
|
| 248 |
-
if not args.quiet:
|
| 249 |
-
print(f"{name} score matching")
|
| 250 |
-
objectives[name] = (model, train(model, loss_fn, args.steps, args.lr, quiet=args.quiet))
|
| 251 |
-
|
| 252 |
-
# Every fitted field is measured against the same exact score on one held-out batch.
|
| 253 |
-
clean = sample_clean(4096, generator) # [4096, 1].
|
| 254 |
-
noise = torch.randn(clean.shape, generator=generator) # [4096, 1].
|
| 255 |
-
evaluation = A_T * clean + SIGMA_T * noise # [4096, 1] samples of p_t.
|
| 256 |
-
target = exact_score(evaluation)
|
| 257 |
-
errors = {n: float((model_score(m, evaluation).detach() - target).square().mean().sqrt())
|
| 258 |
-
for n, (m, _) in objectives.items()}
|
| 259 |
-
|
| 260 |
-
identity = integration_by_parts_gap(lambda z: model_score(objectives["explicit"][0], z),
|
| 261 |
-
evaluation)
|
| 262 |
-
wide = torch.Generator().manual_seed(args.seed + 2) # The family scan has its own draws.
|
| 263 |
-
scan_clean = sample_clean(32768, wide) # [32768, 1].
|
| 264 |
-
scan = minimizers(scan_clean, torch.randn(scan_clean.shape, generator=wide))
|
| 265 |
-
|
| 266 |
-
means = torch.randn(2, TRACE_DIM, generator=generator) * 1.5 # [2, d] two distant modes.
|
| 267 |
-
points = torch.randn(8, TRACE_DIM, generator=generator) # [8, d] evaluation points.
|
| 268 |
-
field = lambda z: high_dimensional_score(z, means, 1.0) # noqa: E731
|
| 269 |
-
exact, passes = exact_trace(field, points)
|
| 270 |
-
estimate, draws = hutchinson_trace(field, points, args.probes, generator)
|
| 271 |
-
|
| 272 |
-
posterior = posterior_report(7.4, (10.0, 9.0), (0.3, 0.7), epsilon_theta=-0.8)
|
| 273 |
-
report = {
|
| 274 |
-
"parameters per model": count_parameters(objectives["explicit"][0]),
|
| 275 |
-
"score error, the three fits": [round(errors[n], 4) for n in NAMES],
|
| 276 |
-
"E[s . grad log p] and -E[div s]": [round(identity[k], 4) for k in ("left", "right")],
|
| 277 |
-
"integration by parts gap": identity["gap"],
|
| 278 |
-
"minimizing a, the three forms": [round(scan[n], 4) for n in NAMES], # All near 1.
|
| 279 |
-
# The one-parameter fit at a = 0.5 and a = 1 on unit variance, then at a = 1/v with v = 4.
|
| 280 |
-
"one-parameter losses": [round(one_parameter_loss(a, v), 4)
|
| 281 |
-
for a, v in ((0.5, 1.0), (1.0, 1.0), (0.25, 4.0))],
|
| 282 |
-
"exact trace, backward passes": passes, # 64, one per coordinate.
|
| 283 |
-
"exact trace, mean": float(exact.mean()),
|
| 284 |
-
"Hutchinson trace, mean": float(estimate.mean()),
|
| 285 |
-
"Hutchinson standard error": float(draws.std(dim=0).mean() / math.sqrt(args.probes)),
|
| 286 |
-
"posterior weights at x_t=7.4": posterior["weights"], # 0.2156 and 0.7844.
|
| 287 |
-
"posterior mean of x_0": posterior["clean_mean"], # 9.2156.
|
| 288 |
-
"conditional scores": posterior["conditional_scores"], # 1.6667 and -0.5556.
|
| 289 |
-
"marginal score, two routes": [round(v, 4) for v in posterior["marginal"]], # -0.0765.
|
| 290 |
-
"irreducible residual": posterior["residual"], # 0.835.
|
| 291 |
-
"score from eps_theta=-0.8": posterior["score_hat"], # 1.3333.
|
| 292 |
-
"x_0 estimates": [round(v, 4) for v in posterior["x0"]], # 9.85, 9.85 and 9.22.
|
| 293 |
-
}
|
| 294 |
-
if not args.quiet:
|
| 295 |
-
print(f"\ndevice={device} level a_t={A_T} sigma_t={SIGMA_T} seed={args.seed}")
|
| 296 |
-
print_report("Three objectives, one score field", report)
|
| 297 |
-
save_run(args.out, config=vars(args), report=report, losses=objectives["denoising"][1])
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
# %% 11. Run the script
|
| 301 |
-
if __name__ == "__main__":
|
| 302 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_3/sde.py
DELETED
|
@@ -1,249 +0,0 @@
|
|
| 1 |
-
"""The forward corruption as a stochastic differential equation.
|
| 2 |
-
|
| 3 |
-
A deterministic flow is fixed by its velocity at every point. A random path needs one more
|
| 4 |
-
ingredient, the perturbation applied over each small interval. A Brownian increment of an interval
|
| 5 |
-
of length h has size sqrt(h), one Euler-Maruyama step adds that increment to the Euler step of
|
| 6 |
-
Chapter 2, a population started at one point spreads at a rate the noise scale sets, with a
|
| 7 |
-
restoring drift that spread stays bounded, and under Ito's lemma and the Fokker-Planck equation
|
| 8 |
-
a single drift and noise scale become an equation for the density. No network appears here.
|
| 9 |
-
|
| 10 |
-
Chapter 3 runs its diffusion clock from the data, so t = 0 holds a clean sample and corruption
|
| 11 |
-
grows with t. Every time in this file is read that way.
|
| 12 |
-
|
| 13 |
-
Chapter 3, Sections 3.2 through 3.10 of the notes. Prints the chapter's worked step from x = 2, the
|
| 14 |
-
Ornstein-Uhlenbeck mean and variance at t = log 2, the Ito check on E[W_t^2], and the
|
| 15 |
-
Fokker-Planck estimate of the Gaussian peak at t = 0.1.
|
| 16 |
-
|
| 17 |
-
python lecture_3/sde.py # 400 Euler-Maruyama steps, 16384 paths
|
| 18 |
-
python lecture_3/sde.py --steps 20 # a coarse grid, for a first look
|
| 19 |
-
"""
|
| 20 |
-
|
| 21 |
-
# %% 01. Imports and configuration
|
| 22 |
-
import math
|
| 23 |
-
|
| 24 |
-
import torch
|
| 25 |
-
|
| 26 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed
|
| 27 |
-
|
| 28 |
-
# `--steps` is the number of Euler-Maruyama steps on the grid and `--samples` the number of paths;
|
| 29 |
-
# nothing in this file is trained, so those two flags carry the whole cost of a run.
|
| 30 |
-
TERMINAL_TIME = 1.0 # T, the end of the simulated interval.
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
# %% 02. Drift and Brownian increments
|
| 34 |
-
#
|
| 35 |
-
# dX_t = f(X_t, t) dt + g(t) dW_t. The drift f fixes the average displacement over a short interval
|
| 36 |
-
# and the Brownian increment dW_t spreads the possible next states around that average. Setting
|
| 37 |
-
# g = 0 recovers the ordinary differential equation of Chapter 2, with f as the velocity field.
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def brownian_increment(h: float, shape: tuple[int, ...], generator=None) -> torch.Tensor:
|
| 41 |
-
"""One Brownian increment over an interval of length h, drawn as sqrt(h) * epsilon."""
|
| 42 |
-
epsilon = torch.randn(shape, generator=generator) # [*shape] standard Gaussian draw.
|
| 43 |
-
return math.sqrt(h) * epsilon # Covariance h I, so each coordinate has variance h.
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
# %% 03. The size of a Brownian increment
|
| 47 |
-
#
|
| 48 |
-
# Multiplying a standard Gaussian by sqrt(h) multiplies its covariance by h, so a coordinate of the
|
| 49 |
-
# increment has standard deviation sqrt(h). Halving the interval divides the typical displacement
|
| 50 |
-
# by sqrt(2) = 1.414, and the ratio sqrt(h)/h = 1/sqrt(h) grows without bound as the grid refines,
|
| 51 |
-
# which is why dW_t appears in the equation and never a velocity dW_t/dt.
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def increment_scale(h: float, f_value: float = 1.0, g_value: float = 1.0) -> dict:
|
| 55 |
-
"""The standard deviation of one coordinate of the increment, and two ratios built from it."""
|
| 56 |
-
std = math.sqrt(h) # Typical displacement of the noise over an interval of length h.
|
| 57 |
-
return {
|
| 58 |
-
"std": std,
|
| 59 |
-
"std_over_h": std / h, # Grows without bound as the grid refines.
|
| 60 |
-
"noise_over_drift": g_value * std / (abs(f_value) * h), # g / (|f| sqrt(h)).
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
# %% 04. The Euler-Maruyama step
|
| 65 |
-
#
|
| 66 |
-
# x_{k+1} = x_k + f(x_k, t_k) h + g(t_k) sqrt(h) epsilon_k. The drift term is the Euler step of
|
| 67 |
-
# Chapter 2 unchanged and the noise term is the only addition, with the exact law of a Brownian
|
| 68 |
-
# increment over an interval of length h.
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def euler_maruyama_step(x_k, f_value, g_value, h: float, epsilon):
|
| 72 |
-
"""One step of the scheme, with the Gaussian draw epsilon_k supplied by the caller."""
|
| 73 |
-
drift_part = f_value * h # Displacement the drift alone would produce.
|
| 74 |
-
noise_part = g_value * math.sqrt(h) * epsilon # Displacement from this draw.
|
| 75 |
-
return x_k + drift_part + noise_part, drift_part, noise_part
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
# %% 05. Fresh randomness and the spreading cloud
|
| 79 |
-
#
|
| 80 |
-
# Each step draws a new epsilon_k, so two runs from the same state give two trajectories. Started
|
| 81 |
-
# from a common point x, the next state is Gaussian with mean x + f h and covariance g^2 h I, so
|
| 82 |
-
# the drift moves the center and the noise sets the width. Without a drift the variances of
|
| 83 |
-
# independent increments add, so after N steps the variance is N g^2 h = g^2 T and is unbounded.
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def driftless_cloud(num_paths: int, steps: int, g_value: float, terminal: float, generator=None):
|
| 87 |
-
"""Simulate f = 0 on a grid of ``steps`` intervals and return the terminal states. [N]."""
|
| 88 |
-
h = terminal / steps # Step size, a time.
|
| 89 |
-
x = torch.zeros(num_paths) # [N] every path starts at the origin.
|
| 90 |
-
for _ in range(steps): # Independent increments accumulate additively in variance.
|
| 91 |
-
x = x + g_value * brownian_increment(h, (num_paths,), generator)
|
| 92 |
-
return x # [N] terminal states, with variance g^2 T in the limit of many paths.
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
# %% 06. A restoring drift
|
| 96 |
-
#
|
| 97 |
-
# dX_t = -kappa X_t dt + g dW_t. Multiplying by the integrating factor e^{kappa t} clears the
|
| 98 |
-
# drift, and under the Ito isometry the variance of the remaining stochastic integral is an
|
| 99 |
-
# ordinary integral, so X_t given x_0 is Gaussian with mean e^{-kappa t} x_0 and variance
|
| 100 |
-
# g^2 (1 - e^{-2 kappa t})/(2 kappa). The stationary variance g^2/(2 kappa) is the level at
|
| 101 |
-
# which the inward pull of the drift offsets the outward spread of the noise.
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
def ornstein_uhlenbeck_law(x_0: float, kappa: float, g_value: float, t: float) -> dict:
|
| 105 |
-
"""The exact conditional mean and variance of the Ornstein-Uhlenbeck process at time t."""
|
| 106 |
-
decay = math.exp(-kappa * t) # Fraction of the initial signal that remains.
|
| 107 |
-
stationary = g_value ** 2 / (2.0 * kappa) # Variance the process settles on.
|
| 108 |
-
variance = stationary * (1.0 - decay ** 2) # Rises from zero toward the stationary value.
|
| 109 |
-
return {"mean": decay * x_0, "variance": variance, "std": math.sqrt(variance),
|
| 110 |
-
"stationary_variance": stationary}
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def simulate_ornstein_uhlenbeck(num_paths, steps, x_0, kappa, g_value, t, generator=None):
|
| 114 |
-
"""Euler-Maruyama on the same process, for comparison with the closed form. Returns [N]."""
|
| 115 |
-
h = t / steps
|
| 116 |
-
x = torch.full((num_paths,), float(x_0)) # [N] all paths start at x_0.
|
| 117 |
-
for _ in range(steps):
|
| 118 |
-
x = x - kappa * x * h + g_value * brownian_increment(h, (num_paths,), generator)
|
| 119 |
-
return x # [N] states at time t.
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
# %% 07. Ito's lemma
|
| 123 |
-
#
|
| 124 |
-
# d phi(X_t) = (phi_t + f phi_x + (1/2) g^2 phi_xx) dt + g phi_x dW_t. The squared increment
|
| 125 |
-
# (Delta X)^2 tends to g^2 Delta t and does not vanish, so the second-order Taylor term is as
|
| 126 |
-
# large as the first-order drift term and belongs in the dt part. The chapter's test function is
|
| 127 |
-
# phi(x) = x^2 on X_t = W_t, where phi_x = 2x and phi_xx = 2, giving d(W_t^2) = dt + 2 W_t dW_t.
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
def ito_drift(f_value, g_value, phi_x, phi_xx):
|
| 131 |
-
"""The dt bracket of Ito's lemma for a probe with no explicit time dependence."""
|
| 132 |
-
return f_value * phi_x + 0.5 * g_value ** 2 * phi_xx # Drift term plus curvature correction.
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def check_ito_on_square(num_paths: int, steps: int, terminal: float, generator=None) -> dict:
|
| 136 |
-
"""Integrate d(W_t^2) = dt + 2 W_t dW_t on a grid and compare with the exact E[W_t^2] = t."""
|
| 137 |
-
h = terminal / steps
|
| 138 |
-
w = torch.zeros(num_paths) # [N] Brownian position.
|
| 139 |
-
accumulated = torch.zeros(num_paths) # [N] right-hand side of Ito's lemma, integrated.
|
| 140 |
-
without_curvature = torch.zeros(num_paths) # [N] the same sum with the dt term dropped.
|
| 141 |
-
for _ in range(steps):
|
| 142 |
-
dw = brownian_increment(h, (num_paths,), generator) # [N] one increment.
|
| 143 |
-
accumulated = accumulated + ito_drift(0.0, 1.0, 2.0 * w, 2.0) * h + 2.0 * w * dw
|
| 144 |
-
without_curvature = without_curvature + 2.0 * w * dw # Ordinary chain rule.
|
| 145 |
-
w = w + dw # Advance the path itself.
|
| 146 |
-
return {
|
| 147 |
-
"realized_mean_square": float((w ** 2).mean()), # Direct average of W_T^2.
|
| 148 |
-
"ito_mean": float(accumulated.mean()), # Same quantity through Ito's lemma.
|
| 149 |
-
"no_curvature_mean": float(without_curvature.mean()), # Near zero, the wrong answer.
|
| 150 |
-
"exact_mean_square": terminal, # E[W_T^2] = T.
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
# %% 08. The Fokker-Planck equation
|
| 155 |
-
#
|
| 156 |
-
# d_t p_t = -div(f p_t) + (1/2) g^2 laplacian(p_t). With f = 0 and g = 1 started from N(0, 1), the
|
| 157 |
-
# solution stays Gaussian with variance 1 + t, which gives both sides in closed form. Autograd
|
| 158 |
-
# supplies the two derivatives, so the identity is checked and not assumed.
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
def spreading_gaussian_log_density(x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 162 |
-
"""log N(x; 0, 1 + t), the exact solution of d_t p = (1/2) d_x^2 p from N(0, 1)."""
|
| 163 |
-
variance = 1.0 + t # Variance grows at the rate the diffusion injects it.
|
| 164 |
-
return -0.5 * torch.log(2.0 * math.pi * variance) - x ** 2 / (2.0 * variance)
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
def fokker_planck_residual(x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 168 |
-
"""d_t p - (1/2) d_x^2 p on the closed-form solution, which is zero at every (x, t)."""
|
| 169 |
-
x = x.clone().requires_grad_(True) # [M] evaluation points.
|
| 170 |
-
t = t.clone().requires_grad_(True) # [M] evaluation times.
|
| 171 |
-
p = spreading_gaussian_log_density(x, t).exp() # [M] the density itself.
|
| 172 |
-
ones = torch.ones_like(p)
|
| 173 |
-
dp_dt, dp_dx = torch.autograd.grad(p, (t, x), ones, create_graph=True) # First derivatives.
|
| 174 |
-
d2p_dx2, = torch.autograd.grad(dp_dx, x, torch.ones_like(dp_dx)) # Second derivative in x.
|
| 175 |
-
return (dp_dt - 0.5 * d2p_dx2).detach() # The equation with f = 0 and g = 1.
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
def peak_extrapolation(step: float) -> dict:
|
| 179 |
-
"""The chapter's first-order estimate of the peak density after a short interval."""
|
| 180 |
-
p_0 = 1.0 / math.sqrt(2.0 * math.pi) # p_0(0), the height of the standard Gaussian.
|
| 181 |
-
curvature = -p_0 # p_0''(x) = (x^2 - 1) p_0(x), so p_0''(0) = -p_0(0).
|
| 182 |
-
rate = 0.5 * 1.0 * curvature # d_t p_t(0) at t = 0, with g = 1.
|
| 183 |
-
linear = p_0 + step * rate # One-step extrapolation from the rate.
|
| 184 |
-
exact = 1.0 / math.sqrt(2.0 * math.pi * (1.0 + step)) # Peak of N(0, 1 + step).
|
| 185 |
-
return {"p_0": p_0, "rate": rate, "linear": linear, "exact": exact, "gap": exact - linear}
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
# %% 09. The report
|
| 189 |
-
def main() -> None:
|
| 190 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 191 |
-
parser.set_defaults(steps=400, samples=16384)
|
| 192 |
-
args = parser.parse_args()
|
| 193 |
-
set_seed(args.seed)
|
| 194 |
-
device = pick_device(args.device) # Printed for consistency; this file runs on the CPU.
|
| 195 |
-
generator = torch.Generator().manual_seed(args.seed) # Local generator, so the report repeats.
|
| 196 |
-
|
| 197 |
-
# The chapter's step: x_k = 2, f = -1, g = 0.5, h = 0.04 and the draw epsilon_k = 1.5.
|
| 198 |
-
h = 0.04
|
| 199 |
-
landed, drift_part, noise_part = euler_maruyama_step(2.0, -1.0, 0.5, h, 1.5)
|
| 200 |
-
scale = increment_scale(h, f_value=-1.0, g_value=0.5)
|
| 201 |
-
three_draws = [euler_maruyama_step(2.0, -1.0, 0.5, h, e)[0] for e in (1.5, -0.5, 0.2)]
|
| 202 |
-
|
| 203 |
-
# The cloud without a drift: variance g^2 T after the whole interval.
|
| 204 |
-
terminal_states = driftless_cloud(args.samples, args.steps, 0.5, TERMINAL_TIME, generator)
|
| 205 |
-
|
| 206 |
-
# The Ornstein-Uhlenbeck example: x_0 = 2, kappa = 1, g = sqrt(2), evaluated at t = log 2.
|
| 207 |
-
ou_time = math.log(2.0)
|
| 208 |
-
ou = ornstein_uhlenbeck_law(2.0, 1.0, math.sqrt(2.0), ou_time)
|
| 209 |
-
ou_paths = simulate_ornstein_uhlenbeck(
|
| 210 |
-
args.samples, args.steps, 2.0, 1.0, math.sqrt(2.0), ou_time, generator,
|
| 211 |
-
)
|
| 212 |
-
|
| 213 |
-
ito = check_ito_on_square(args.samples, args.steps, 0.25, generator) # t = 0.25.
|
| 214 |
-
grid = torch.linspace(-3.0, 3.0, 61) # [61] evaluation points for the density equation.
|
| 215 |
-
residual = fokker_planck_residual(grid, torch.full_like(grid, 0.5)) # At t = 0.5.
|
| 216 |
-
peak = peak_extrapolation(0.1)
|
| 217 |
-
|
| 218 |
-
report = {
|
| 219 |
-
"euler_maruyama x_{k+1}": landed, # 2.11 in the notes.
|
| 220 |
-
"drift displacement f h": drift_part, # -0.04.
|
| 221 |
-
"noise displacement g sqrt(h) eps": noise_part, # 0.15.
|
| 222 |
-
"typical noise over drift": scale["noise_over_drift"], # 2.5 at this step size.
|
| 223 |
-
"increment std at h=0.04": scale["std"], # 0.2.
|
| 224 |
-
"three endpoints, mean": sum(three_draws) / 3.0, # 2.00 against the drift's 1.96.
|
| 225 |
-
"driftless terminal variance": float(terminal_states.var(unbiased=False)), # g^2 T = 0.25.
|
| 226 |
-
"OU mean at t=log 2": ou["mean"], # 1, half the initial signal.
|
| 227 |
-
"OU variance at t=log 2": ou["variance"], # 0.75 of the stationary 1.
|
| 228 |
-
"OU simulated mean": float(ou_paths.mean()),
|
| 229 |
-
"OU simulated variance": float(ou_paths.var(unbiased=False)),
|
| 230 |
-
"E[W_t^2] at t=0.25, exact": ito["exact_mean_square"], # 0.25.
|
| 231 |
-
"E[W_t^2] simulated directly": ito["realized_mean_square"],
|
| 232 |
-
"E[W_t^2] through Ito's lemma": ito["ito_mean"],
|
| 233 |
-
"E[W_t^2] without the curvature": ito["no_curvature_mean"], # Near zero.
|
| 234 |
-
"Fokker-Planck residual, largest": float(residual.abs().max()), # Zero up to float error.
|
| 235 |
-
"peak p_0(0)": peak["p_0"], # 0.3989.
|
| 236 |
-
"peak rate at t=0": peak["rate"], # -0.1995.
|
| 237 |
-
"peak linear estimate at t=0.1": peak["linear"], # 0.3790.
|
| 238 |
-
"peak exact at t=0.1": peak["exact"], # 0.3804.
|
| 239 |
-
"second-order gap": peak["gap"], # 0.0014.
|
| 240 |
-
}
|
| 241 |
-
if not args.quiet:
|
| 242 |
-
print(f"device={device} steps={args.steps} paths={args.samples} seed={args.seed}")
|
| 243 |
-
print_report("Forward corruption without a network", report)
|
| 244 |
-
save_run(args.out, config=vars(args), report=report)
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
# %% 10. Run the script
|
| 248 |
-
if __name__ == "__main__":
|
| 249 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|