Delete lecture_2
Browse files- lecture_2/README.md +0 -99
- lecture_2/flow_matching_2d.py +0 -245
- lecture_2/flow_matching_mnist.py +0 -230
- lecture_2/interpolants.py +0 -210
- lecture_2/velocity_fields.py +0 -263
lecture_2/README.md
DELETED
|
@@ -1,99 +0,0 @@
|
|
| 1 |
-
# Lecture 2: Flow Matching (Chapter 2)
|
| 2 |
-
|
| 3 |
-
A velocity field assigns one vector to every state and every time. Following it from a starting
|
| 4 |
-
point traces a trajectory, the trajectories from every starting point form the flow map, and the
|
| 5 |
-
continuity equation ties the motion of the density to the same field. Training such a field by
|
| 6 |
-
maximum likelihood costs an ODE solve and a divergence trace per gradient step, so flow matching
|
| 7 |
-
replaces it with a regression: pair a Gaussian sample with a data sample, join the pair by a
|
| 8 |
-
straight line, and regress a network onto the derivative of that line. Least squares recovers the
|
| 9 |
-
average velocity of all the paths through a state, which is the field that transports the prior
|
| 10 |
-
onto the data, and generation is Euler integration of the learned field.
|
| 11 |
-
|
| 12 |
-
`flow_matching_mnist.py` is the lecture's main script, the one Section 2.20 of the notes walks
|
| 13 |
-
through line by line. The other three files are the mathematics underneath it: the fields and the
|
| 14 |
-
solver before any training, the full method on a target whose density is known exactly, and the
|
| 15 |
-
family of interpolants the straight line belongs to.
|
| 16 |
-
|
| 17 |
-
| File | Command | What it does |
|
| 18 |
-
| --- | --- | --- |
|
| 19 |
-
| `flow_matching_mnist.py` | `python lecture_2/flow_matching_mnist.py` | Trains the small U-Net on MNIST and writes a 4-by-4 grid of digits and a trajectory strip. `--dataset toy` runs offline. |
|
| 20 |
-
| `velocity_fields.py` | `python lecture_2/velocity_fields.py` | No training. Trajectories, the flow map, the Euler refinement ladder, the pushforward of a Gaussian, the flux balance and the continuity equation. |
|
| 21 |
-
| `flow_matching_2d.py` | `python lecture_2/flow_matching_2d.py` | Trains a velocity MLP on the four-Gaussian target and samples by Euler. Measures the conditional flow-matching equivalence against the exact marginal velocity. |
|
| 22 |
-
| `interpolants.py` | `python lecture_2/interpolants.py` | Trains the same MLP three times, on the linear, the t-squared and the trigonometric schedule, and compares the samples at a fine and a coarse step budget. |
|
| 23 |
-
|
| 24 |
-
Nothing is trained in `velocity_fields.py`, so its `--steps` sets the largest rung of the Euler
|
| 25 |
-
refinement ladder. In the other three files `--steps` counts optimizer steps, and in
|
| 26 |
-
`flow_matching_mnist.py` it is the budget for the whole run, which `--epochs` would otherwise set.
|
| 27 |
-
|
| 28 |
-
## The numbers a default run produces
|
| 29 |
-
|
| 30 |
-
`velocity_fields.py`, in about two seconds, reproduces the chapter's arithmetic exactly.
|
| 31 |
-
|
| 32 |
-
```
|
| 33 |
-
euler steps 1 landing 2.000000 error 0.718282 error x steps 0.7183
|
| 34 |
-
euler steps 2 landing 2.250000 error 0.468282 error x steps 0.9366
|
| 35 |
-
euler steps 4 landing 2.441406 error 0.276876 error x steps 1.1075
|
| 36 |
-
flux_balance -0.3354 enclosed_mass_rate -0.3354
|
| 37 |
-
continuity_time_derivative 0.1123 continuity_minus_flux_divergence 0.1123
|
| 38 |
-
density_ratio_at_origin 2.0000 log_density_change -0.6931
|
| 39 |
-
```
|
| 40 |
-
|
| 41 |
-
`flow_matching_2d.py`, in about six seconds on one core, with 83,202 parameters.
|
| 42 |
-
|
| 43 |
-
```
|
| 44 |
-
crossing_conditional_loss [10.0, 9.0, 10.0] crossing_marginal_loss [1.0, 0.0, 1.0]
|
| 45 |
-
crossing_constant_gap [9.0, 9.0, 9.0] crossing_target_variance 9.0000
|
| 46 |
-
loss_cfm 10.3243 loss_fm 5.1196 loss_gap 5.2506 loss_cross_term -0.0459
|
| 47 |
-
gradient_cosine 0.9989 final_loss 5.3169
|
| 48 |
-
modes_found 4 mode_entropy 1.3657 nearest_mode_distance 0.4505
|
| 49 |
-
mean_log_density -2.2648 data_mean_log_density -2.1402
|
| 50 |
-
```
|
| 51 |
-
|
| 52 |
-
`interpolants.py`, in about thirteen seconds, trains three fields from the same initialization.
|
| 53 |
-
|
| 54 |
-
```
|
| 55 |
-
endpoint_error covariance_deviation mean_log_density coarse_log_density
|
| 56 |
-
linear 0.0000 0.4999 -2.5276 -3.0309
|
| 57 |
-
quadratic 0.0000 0.4939 -2.5748 -3.6419
|
| 58 |
-
trigonometric 0.0000 0.0179 -2.6621 -2.9566
|
| 59 |
-
```
|
| 60 |
-
|
| 61 |
-
All three schedules reach the four modes. The trigonometric one is the only schedule whose
|
| 62 |
-
interpolation keeps identity covariance at every time, and it is the one that loses least when the
|
| 63 |
-
sampler drops from 64 Euler steps to 8, while the t-squared schedule, whose velocity grows linearly
|
| 64 |
-
in t, loses most.
|
| 65 |
-
|
| 66 |
-
`flow_matching_mnist.py` downloads MNIST on its first run. The offline path, `--dataset toy`,
|
| 67 |
-
trains the same 471,265-parameter U-Net on 512 bars and blobs in about two minutes on one core.
|
| 68 |
-
|
| 69 |
-
```
|
| 70 |
-
epoch=20 squared_norm_loss=977.133
|
| 71 |
-
final loss 977.133 sample range [-5.15, 3.50]
|
| 72 |
-
```
|
| 73 |
-
|
| 74 |
-
Twenty epochs over 512 images is 80 optimizer steps, and the generated range is still wider than
|
| 75 |
-
the data's [-1, 1]. With `--epochs 100` the loss reaches 387.136 and the range [-3.25, 3.04], in
|
| 76 |
-
about nine minutes. The MNIST default is 20 epochs over 60,000 images, and it is the one script
|
| 77 |
-
here that does not finish while you watch it. `--steps 20` trains for twenty optimizer steps and
|
| 78 |
-
then samples, which is the quick check that the file runs.
|
| 79 |
-
|
| 80 |
-
## What is simplified
|
| 81 |
-
|
| 82 |
-
- The marginal velocity `v_t(x) = E[U_t | X_t = x]` is written in closed form in
|
| 83 |
-
`flow_matching_2d.py`. That is available because the target is a Gaussian mixture and the prior is
|
| 84 |
-
Gaussian; Section 2.14 of the notes is about the case where it is not, which is why the equivalence
|
| 85 |
-
it proves is worth proving.
|
| 86 |
-
- The equivalence is measured on one large batch, so the two losses agree up to that batch's own
|
| 87 |
-
sampling error. The file prints the cross term that accounts for the residual exactly.
|
| 88 |
-
- The identity-covariance check uses two independent standard Gaussian endpoints, which is the
|
| 89 |
-
hypothesis the chapter states. The four-Gaussian target is centered but not white.
|
| 90 |
-
- `velocity_fields.py` differentiates the densities with autograd where the chapter differentiates
|
| 91 |
-
by hand, so no derivative in that file is transcribed from the notes. It also runs in double
|
| 92 |
-
precision, because the Euler ladder it prints is arithmetic quoted to six decimal places.
|
| 93 |
-
- The Gaussian translation of Section 2.17 is left out. Its coupling is deterministic, so the
|
| 94 |
-
conditional velocity is the constant 4 at every state and time and already equals the marginal
|
| 95 |
-
velocity.
|
| 96 |
-
- The cubic Hermite interpolant of Section 2.18 is left out of `interpolants.py`. It needs endpoint
|
| 97 |
-
slopes, which the four-Gaussian data do not carry.
|
| 98 |
-
- The three two-dimensional scripts run on the CPU. Each state there has two coordinates, so a GPU
|
| 99 |
-
would be slower. `flow_matching_mnist.py` picks CUDA, then Apple MPS, then the CPU.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_2/flow_matching_2d.py
DELETED
|
@@ -1,245 +0,0 @@
|
|
| 1 |
-
"""Flow matching on four Gaussians, the whole method end to end.
|
| 2 |
-
|
| 3 |
-
A coupling pairs a Gaussian prior sample with a data sample, a straight line joins the pair, and the
|
| 4 |
-
derivative of that line is the regression target. Least squares against those per-pair velocities
|
| 5 |
-
recovers the marginal velocity field, and Euler integration of the learned field turns fresh noise
|
| 6 |
-
into data. The four-Gaussian target is small enough that the marginal velocity has a closed form, so
|
| 7 |
-
the conditional flow-matching equivalence can be measured here and not only argued.
|
| 8 |
-
|
| 9 |
-
Chapter 2, Sections 2.11 to 2.16 and 2.19 of the notes. Trained on
|
| 10 |
-
``cis6270.data.gaussian_mixture_2d``. Prints the worked endpoint pair, the average at the crossing,
|
| 11 |
-
the two losses with their constant gap, the agreement of their gradients, and the mode coverage and
|
| 12 |
-
mean exact log density of the generated points.
|
| 13 |
-
|
| 14 |
-
python lecture_2/flow_matching_2d.py # train and generate, about ten seconds
|
| 15 |
-
python lecture_2/flow_matching_2d.py --steps 20 # a first look, with no time to learn
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
import argparse
|
| 20 |
-
|
| 21 |
-
import torch
|
| 22 |
-
|
| 23 |
-
from cis6270.data import GAUSSIAN_CENTERS, GAUSSIAN_STD, gaussian_mixture_2d
|
| 24 |
-
from cis6270.metrics import mean_log_density, mode_report
|
| 25 |
-
from cis6270.nets import VelocityMLP, count_parameters
|
| 26 |
-
from cis6270.runner import print_report, save_run, set_seed, train
|
| 27 |
-
|
| 28 |
-
DIM = 2 # d, the width of the state.
|
| 29 |
-
TRAIN_STEPS = 1000 # Optimizer steps, each on one fresh batch of endpoint pairs.
|
| 30 |
-
BATCH_SIZE = 256 # Endpoint pairs per step.
|
| 31 |
-
LEARNING_RATE = 3e-3 # Optimizer step size.
|
| 32 |
-
WIDTH = 128 # Hidden width of the velocity network.
|
| 33 |
-
SAMPLE_STEPS = 64 # Euler steps from t = 0 to t = 1, so Delta t = 1/64.
|
| 34 |
-
NUM_SAMPLES = 1024 # Points generated for the mode coverage and the log density.
|
| 35 |
-
DATA_POINTS = 4096 # Size of the training set drawn from the four-Gaussian target.
|
| 36 |
-
EQUIVALENCE_POINTS = 16384 # Pairs used for the Monte Carlo check of Theorem 2.1.
|
| 37 |
-
GRID = torch.tensor([0.0, 0.25, 0.50, 0.75, 1.0]) # [5] the chapter's grid of times.
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
# %% 02. The coupling
|
| 41 |
-
#
|
| 42 |
-
# Section 2.11. A coupling is any joint law over the endpoint pair whose marginals are the prior and
|
| 43 |
-
# the data. Drawing the two ends with no reference to each other gives the independent coupling.
|
| 44 |
-
def sample_coupling(data, batch_size):
|
| 45 |
-
"""Draw (X_0, X_1) ~ pi with pi(x_0, x_1) = p_0(x_0) p_1(x_1). Returns two [B, d] tensors."""
|
| 46 |
-
index = torch.randint(0, len(data), (batch_size,), device=data.device) # [B] which data point.
|
| 47 |
-
x_1 = data[index] # [B, d] the data endpoint, drawn from p_1.
|
| 48 |
-
x_0 = torch.randn_like(x_1) # [B, d] the prior endpoint, drawn from p_0 = N(0, I).
|
| 49 |
-
return x_0, x_1
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
# %% 03. The conditional path and its velocity
|
| 53 |
-
#
|
| 54 |
-
# Section 2.12. The straight segment at constant speed, and its derivative with the endpoints held
|
| 55 |
-
# fixed. The velocity is called conditional because only someone who knows both endpoints has it.
|
| 56 |
-
def conditional_path(x_0, x_1, t):
|
| 57 |
-
"""X_t = (1 - t) X_0 + t X_1 and U_t = X_1 - X_0. Returns two [B, d] tensors."""
|
| 58 |
-
x_t = (1.0 - t)[:, None] * x_0 + t[:, None] * x_1 # [B, d] convex combination of the endpoints.
|
| 59 |
-
u_t = x_1 - x_0 # [B, d] the displacement, the same vector at every time.
|
| 60 |
-
return x_t, u_t
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def worked_pair():
|
| 64 |
-
"""The chapter's one-dimensional pair x_0 = -2, x_1 = 3, evaluated on the grid of times."""
|
| 65 |
-
x_0 = torch.full((len(GRID), 1), -2.0) # [5, 1] the same prior endpoint at every grid time.
|
| 66 |
-
x_1 = torch.full((len(GRID), 1), 3.0) # [5, 1] the same data endpoint at every grid time.
|
| 67 |
-
x_t, u_t = conditional_path(x_0, x_1, GRID) # The line x_t = -2 + 5t and its slope.
|
| 68 |
-
return {
|
| 69 |
-
"worked_pair_path": [round(float(v), 4) for v in x_t[:, 0]],
|
| 70 |
-
"worked_pair_velocity": float(u_t[0, 0]), # The slope 5, so 1.25 units in every quarter.
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
# %% 04. The marginal velocity
|
| 75 |
-
#
|
| 76 |
-
# Section 2.13. Several conditional paths pass through one state at one time, each with its own
|
| 77 |
-
# U_t, and a deterministic field assigns one vector there, the average v_t(x) = E[U_t | X_t = x].
|
| 78 |
-
# For this target the average is available exactly, and Section 2.14 is about the case where it
|
| 79 |
-
# is not. Conditioning on component k makes (X_1, X_t) jointly Gaussian, so the posterior over
|
| 80 |
-
# components is a softmax of the component log densities at X_t and v_t follows in closed form.
|
| 81 |
-
def marginal_velocity(x_t, t):
|
| 82 |
-
"""v_t(x) = E[U_t | X_t = x] for a standard Gaussian prior and the four-Gaussian target."""
|
| 83 |
-
centers = GAUSSIAN_CENTERS.to(x_t) # [M, d] the four means of the mixture.
|
| 84 |
-
variance = (1.0 - t) ** 2 + (t * GAUSSIAN_STD) ** 2 # [B] variance of X_t given component k.
|
| 85 |
-
offset = x_t[:, None, :] - t[:, None, None] * centers # [B, M, d] distance to each shifted mean.
|
| 86 |
-
log_weight = -0.5 * (offset ** 2).sum(-1) / variance[:, None] # [B, M] up to a shared constant.
|
| 87 |
-
weight = torch.softmax(log_weight, dim=1) # [B, M] posterior over components given X_t.
|
| 88 |
-
mean_center = (weight[:, :, None] * centers).sum(1) # [B, d] posterior mean of the center.
|
| 89 |
-
# Writing E[X_1 | X_t] in terms of that posterior mean and cancelling the factor (1 - t) that
|
| 90 |
-
# U_t = (X_1 - X_t)/(1 - t) would otherwise divide by leaves one that stays finite at t = 1.
|
| 91 |
-
return ((1.0 - t)[:, None] * mean_center
|
| 92 |
-
- (1.0 - t - t * GAUSSIAN_STD ** 2)[:, None] * x_t) / variance[:, None] # [B, d].
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def crossing_average():
|
| 96 |
-
"""Two paths of Section 2.13 that meet at x = 2 at t = 0.5, with velocities 4 and -2."""
|
| 97 |
-
x_0 = torch.tensor([[0.0], [3.0]]) # [2, 1] the two prior endpoints.
|
| 98 |
-
x_1 = torch.tensor([[4.0], [1.0]]) # [2, 1] the two data endpoints.
|
| 99 |
-
x_t, u_t = conditional_path(x_0, x_1, torch.full((2,), 0.5)) # Both states land on x = 2.
|
| 100 |
-
return {
|
| 101 |
-
"crossing_state": float(x_t.mean()), # Both paths occupy this state at t = 0.5.
|
| 102 |
-
"crossing_velocities": [round(float(v), 4) for v in u_t[:, 0]],
|
| 103 |
-
"crossing_average_velocity": float(u_t.mean()), # The equally weighted average, 1.
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
# %% 05. The two objectives
|
| 108 |
-
#
|
| 109 |
-
# Sections 2.14 and 2.15. The flow-matching loss regresses on the marginal velocity, which is not
|
| 110 |
-
# available for real data, and the conditional loss regresses on the sampled U_t, which is.
|
| 111 |
-
def objectives(model, x_0, x_1, t):
|
| 112 |
-
"""L_CFM, L_FM, the gap E||U_t - v_t||^2 and the cross term, on one batch of endpoint pairs."""
|
| 113 |
-
x_t, u_t = conditional_path(x_0, x_1, t) # [B, d] the state and its conditional velocity.
|
| 114 |
-
v_pred = model(x_t, t) # [B, d] the network sees the state and the time, never the endpoints.
|
| 115 |
-
v_t = marginal_velocity(x_t, t) # [B, d] the target of the loss we cannot evaluate in general.
|
| 116 |
-
loss_cfm = (v_pred - u_t).square().sum(1).mean() # E||v_theta(X_t, t) - U_t||^2.
|
| 117 |
-
loss_fm = (v_pred - v_t).square().sum(1).mean() # E||v_theta(X_t, t) - v_t(X_t)||^2.
|
| 118 |
-
gap = (u_t - v_t).square().sum(1).mean() # The conditional variance of the target.
|
| 119 |
-
# Expanding the square leaves L_CFM = L_FM + cross + gap on any batch. The proof of Theorem 2.1
|
| 120 |
-
# rests on the cross term having conditional mean zero, so on a large batch it is near zero.
|
| 121 |
-
cross = 2.0 * ((v_pred - v_t) * (v_t - u_t)).sum(1).mean()
|
| 122 |
-
return loss_cfm, loss_fm, gap, cross
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
# %% 06. The equivalence, measured
|
| 126 |
-
#
|
| 127 |
-
# Section 2.15. At the crossing the two losses are parabolas in the prediction a, nine units apart
|
| 128 |
-
# at every a, with one derivative between them. The same identity holds in expectation on the
|
| 129 |
-
# two-dimensional problem, up to the Monte Carlo error of the batch that estimates it.
|
| 130 |
-
def crossing_gap(predictions=(0.0, 1.0, 2.0)):
|
| 131 |
-
"""The conditional and marginal losses at x = 2, t = 0.5, where the targets are 4 and -2."""
|
| 132 |
-
targets = torch.tensor([4.0, -2.0]) # [2] the two conditional velocities meeting there.
|
| 133 |
-
marginal = targets.mean() # The conditional mean of the target, which is 1.
|
| 134 |
-
conditional_values, marginal_values = [], []
|
| 135 |
-
for a in predictions: # One candidate prediction at a time.
|
| 136 |
-
conditional_values.append(round(float((a - targets).square().mean()), 4))
|
| 137 |
-
marginal_values.append(round(float((a - marginal) ** 2), 4))
|
| 138 |
-
return {
|
| 139 |
-
"crossing_conditional_loss": conditional_values,
|
| 140 |
-
"crossing_marginal_loss": marginal_values,
|
| 141 |
-
"crossing_constant_gap": [round(c - m, 4) for c, m in
|
| 142 |
-
zip(conditional_values, marginal_values)],
|
| 143 |
-
"crossing_target_variance": float(targets.var(unbiased=False)), # The same constant, 9.
|
| 144 |
-
}
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
def equivalence_check(model, data, num_points):
|
| 148 |
-
"""Theorem 2.1 on the four-Gaussian problem: the gap in value and the agreement in gradient."""
|
| 149 |
-
x_0, x_1 = sample_coupling(data, num_points) # [N, d] pairs from the independent coupling.
|
| 150 |
-
t = torch.rand(num_points, device=x_1.device) # [N] one uniform time per pair.
|
| 151 |
-
loss_cfm, loss_fm, gap, cross = objectives(model, x_0, x_1, t)
|
| 152 |
-
parameters = list(model.parameters())
|
| 153 |
-
grad_cfm = torch.autograd.grad(loss_cfm, parameters, retain_graph=True) # Gradient of L_CFM.
|
| 154 |
-
grad_fm = torch.autograd.grad(loss_fm, parameters) # Gradient of L_FM, on the same batch.
|
| 155 |
-
flat_cfm = torch.cat([g.flatten() for g in grad_cfm]) # [P] one vector per objective.
|
| 156 |
-
flat_fm = torch.cat([g.flatten() for g in grad_fm]) # [P].
|
| 157 |
-
return {
|
| 158 |
-
"loss_cfm": float(loss_cfm.detach()),
|
| 159 |
-
"loss_fm": float(loss_fm.detach()),
|
| 160 |
-
"loss_gap": float(gap.detach()), # The constant that separates the two objectives.
|
| 161 |
-
"loss_cross_term": float(cross.detach()), # Zero in expectation, so small on this batch.
|
| 162 |
-
"loss_fm_plus_gap": float((loss_fm + gap).detach()), # L_CFM once the cross term is added.
|
| 163 |
-
"gradient_cosine": float(torch.nn.functional.cosine_similarity(flat_cfm, flat_fm, dim=0)),
|
| 164 |
-
# What separates the two gradients is the gradient of that cross term, which falls like one
|
| 165 |
-
# over the square root of the batch size.
|
| 166 |
-
"gradient_relative_difference": float((flat_cfm - flat_fm).norm() / flat_fm.norm()),
|
| 167 |
-
}
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
# %% 07. Training
|
| 171 |
-
#
|
| 172 |
-
# Section 2.19. One iteration draws a pair and a time, builds the state in closed form, subtracts
|
| 173 |
-
# the endpoints, and takes a gradient step. No ODE is solved while a training example is built.
|
| 174 |
-
def make_loss(model, data, batch_size):
|
| 175 |
-
"""The closure runner.train calls, returning L_CFM on one fresh batch."""
|
| 176 |
-
def loss_fn(step):
|
| 177 |
-
x_0, x_1 = sample_coupling(data, batch_size) # [B, d] a fresh pair per step.
|
| 178 |
-
t = torch.rand(batch_size, device=x_1.device) # [B] t ~ Uniform(0, 1), one per pair.
|
| 179 |
-
x_t, u_t = conditional_path(x_0, x_1, t) # [B, d] the state and the regression target.
|
| 180 |
-
return (model(x_t, t) - u_t).square().sum(1).mean() # ||v_theta(X_t, t) - U_t||^2.
|
| 181 |
-
return loss_fn
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
# %% 08. Euler sampling
|
| 185 |
-
#
|
| 186 |
-
# Section 2.19. Generation starts from a fresh prior sample and integrates the learned field, so
|
| 187 |
-
# one sample costs as many forward passes as there are steps.
|
| 188 |
-
@torch.no_grad()
|
| 189 |
-
def sample(model, num_samples, steps):
|
| 190 |
-
"""Integrate dX_t/dt = v_theta(X_t, t) from t = 0 to t = 1 on a uniform grid. Returns [N, d]."""
|
| 191 |
-
device = next(model.parameters()).device # Use the model's own device.
|
| 192 |
-
x_t = torch.randn(num_samples, DIM, device=device) # [N, d] the draw X_0 ~ p_0.
|
| 193 |
-
dt = 1.0 / steps # Delta t, the width of one interval.
|
| 194 |
-
for step in range(steps): # One network evaluation per step.
|
| 195 |
-
t = torch.full((num_samples,), step * dt, device=device) # [N] the shared grid time.
|
| 196 |
-
x_t = x_t + dt * model(x_t, t) # Euler: state += time increment times velocity.
|
| 197 |
-
return x_t
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
# %% 09. The report
|
| 201 |
-
def main():
|
| 202 |
-
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
| 203 |
-
parser.add_argument("--steps", type=int, default=TRAIN_STEPS, help="optimizer steps")
|
| 204 |
-
parser.add_argument("--batch-size", type=int, default=BATCH_SIZE, help="pairs per step")
|
| 205 |
-
parser.add_argument("--lr", type=float, default=LEARNING_RATE, help="learning rate")
|
| 206 |
-
parser.add_argument("--width", type=int, default=WIDTH, help="hidden width of the network")
|
| 207 |
-
parser.add_argument("--samples", type=int, default=NUM_SAMPLES, help="points to generate")
|
| 208 |
-
parser.add_argument("--sample-steps", type=int, default=SAMPLE_STEPS, help="Euler steps")
|
| 209 |
-
parser.add_argument("--seed", type=int, default=0, help="random seed")
|
| 210 |
-
parser.add_argument("--out", type=str, default="", help="directory for the run's output")
|
| 211 |
-
parser.add_argument("--quiet", action="store_true", help="print only the final report")
|
| 212 |
-
args = parser.parse_args()
|
| 213 |
-
|
| 214 |
-
set_seed(args.seed) # Seed first, so the same seed gives the same report.
|
| 215 |
-
data = gaussian_mixture_2d(DATA_POINTS, seed=args.seed) # [N, d] the target p_1 = p_data.
|
| 216 |
-
model = VelocityMLP(dim=DIM, width=args.width) # v_theta(x, t), the learned velocity field.
|
| 217 |
-
|
| 218 |
-
report = {"parameters": count_parameters(model)}
|
| 219 |
-
report.update(worked_pair()) # Section 2.12.
|
| 220 |
-
report.update(crossing_average()) # Section 2.13.
|
| 221 |
-
report.update(crossing_gap()) # Section 2.15.
|
| 222 |
-
# The gradient comparison runs before training, where the two gradients are far from zero.
|
| 223 |
-
report.update(equivalence_check(model, data, EQUIVALENCE_POINTS)) # Section 2.15.
|
| 224 |
-
|
| 225 |
-
losses = train(model, make_loss(model, data, args.batch_size),
|
| 226 |
-
steps=args.steps, lr=args.lr, quiet=args.quiet)
|
| 227 |
-
generated = sample(model, args.samples, args.sample_steps) # [N, d] fresh points.
|
| 228 |
-
coverage = mode_report(generated) # Nearest mode, how many modes, how evenly they are visited.
|
| 229 |
-
report.update({
|
| 230 |
-
"final_loss": sum(losses[-100:]) / len(losses[-100:]), # Averaged over the last steps.
|
| 231 |
-
"modes_found": coverage["modes_found"],
|
| 232 |
-
"mode_entropy": coverage["mode_entropy"], # log 4 = 1.3863 when all four are equally used.
|
| 233 |
-
"nearest_mode_distance": coverage["nearest_distance"],
|
| 234 |
-
"mode_fractions": coverage["mode_fractions"],
|
| 235 |
-
"mean_log_density": mean_log_density(generated), # Exact log density of the target.
|
| 236 |
-
"data_mean_log_density": mean_log_density(data), # The same measurement on real points.
|
| 237 |
-
})
|
| 238 |
-
|
| 239 |
-
print_report("flow matching on four Gaussians", report)
|
| 240 |
-
save_run(args.out, model=model, config=vars(args), report=report, losses=losses)
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
# %% 10. Run the script
|
| 244 |
-
if __name__ == "__main__":
|
| 245 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_2/flow_matching_mnist.py
DELETED
|
@@ -1,230 +0,0 @@
|
|
| 1 |
-
"""Flow matching on MNIST, from noise to a handwritten digit in one script.
|
| 2 |
-
|
| 3 |
-
We draw a Gaussian sample at time zero and a real image at time one, join them by a straight line,
|
| 4 |
-
and train a U-Net to predict the velocity of that line at a random time in between. Generation then
|
| 5 |
-
starts from fresh noise and takes Euler steps along the learned field. Chapter 2, Section 2.20 of
|
| 6 |
-
the notes walks through this file section by section.
|
| 7 |
-
|
| 8 |
-
python lecture_2/flow_matching_mnist.py # 20 epochs on MNIST, downloads on first use
|
| 9 |
-
python lecture_2/flow_matching_mnist.py --steps 20 # 20 optimizer steps, for a first look
|
| 10 |
-
python lecture_2/flow_matching_mnist.py --epochs 1 # a single pass, about two minutes on a CPU
|
| 11 |
-
python lecture_2/flow_matching_mnist.py --dataset toy # no download, for a first look or a test
|
| 12 |
-
|
| 13 |
-
The network class and the checkpoint keys match the published `flow_unet_mnist.pt`, so the saved
|
| 14 |
-
five-epoch run still loads.
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
# %% 01. Imports
|
| 18 |
-
import argparse
|
| 19 |
-
from pathlib import Path
|
| 20 |
-
|
| 21 |
-
import torch # Tensors, gradients, and optimization.
|
| 22 |
-
import torch.nn as nn # Layers and model classes.
|
| 23 |
-
import torch.nn.functional as F # Resize feature maps in the decoder.
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
# %% 02. Configuration
|
| 27 |
-
BATCH_SIZE = 128 # Images per training batch.
|
| 28 |
-
EPOCHS = 20 # Passes through the training set.
|
| 29 |
-
LEARNING_RATE = 2e-4 # Optimizer step size.
|
| 30 |
-
BASE_CHANNELS = 32 # U-Net widths: 32, 64, 128.
|
| 31 |
-
SAMPLE_STEPS = 100 # Euler steps from t=0 to t=1.
|
| 32 |
-
NUM_SAMPLES = 16 # Generate a 4-by-4 image grid.
|
| 33 |
-
SEED = 7 # Seed for initialization and training.
|
| 34 |
-
DATA_DIR = Path("data") # MNIST download location.
|
| 35 |
-
OUTPUT_DIR = Path("outputs/flow_matching_mnist") # Checkpoint and generated image folder.
|
| 36 |
-
|
| 37 |
-
DEVICE = torch.device(
|
| 38 |
-
"cuda" if torch.cuda.is_available() # NVIDIA GPU.
|
| 39 |
-
else "mps" if torch.backends.mps.is_available() # Apple GPU.
|
| 40 |
-
else "cpu" # CPU fallback.
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
# %% 03. Load real images
|
| 45 |
-
def make_loader(dataset="mnist", batch_size=BATCH_SIZE):
|
| 46 |
-
"""Batches of images normalized to [-1, 1], the range the velocity field is trained on."""
|
| 47 |
-
from torch.utils.data import DataLoader, TensorDataset
|
| 48 |
-
|
| 49 |
-
if dataset == "toy": # Shapes like MNIST, drawn offline, so a test needs no download.
|
| 50 |
-
from cis6270.data import toy_images
|
| 51 |
-
|
| 52 |
-
images = toy_images(num_images=512, size=28, seed=SEED) # [512, 1, 28, 28].
|
| 53 |
-
labels = torch.zeros(len(images), dtype=torch.long) # Unused; generation is unconditional.
|
| 54 |
-
return DataLoader(TensorDataset(images, labels), batch_size=batch_size, shuffle=True)
|
| 55 |
-
|
| 56 |
-
from torchvision import datasets
|
| 57 |
-
from torchvision.transforms import v2
|
| 58 |
-
|
| 59 |
-
transform = v2.Compose([
|
| 60 |
-
v2.ToImage(), # Channel-first image: [1, 28, 28].
|
| 61 |
-
v2.ToDtype(torch.float32, scale=True), # Integer pixels 0-255 -> floats 0-1.
|
| 62 |
-
v2.Normalize(mean=(0.5,), std=(0.5,)), # Real data pixels: [0, 1] -> [-1, 1].
|
| 63 |
-
])
|
| 64 |
-
mnist = datasets.MNIST(root=DATA_DIR, train=True, download=True, transform=transform)
|
| 65 |
-
return DataLoader(
|
| 66 |
-
mnist, batch_size=batch_size, shuffle=True, # Batch shape: [B, 1, 28, 28].
|
| 67 |
-
num_workers=0, pin_memory=(DEVICE.type == "cuda"), # Load in the main process.
|
| 68 |
-
)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
# %% 04. Sample the conditional path
|
| 72 |
-
def sample_conditional_path(x1): # x1 contains normalized real images.
|
| 73 |
-
x0 = torch.randn_like(x1) # Independent Gaussian prior; same shape as x1.
|
| 74 |
-
t = torch.rand(x1.shape[0], device=x1.device) # One random time per image; [B].
|
| 75 |
-
t_image = t[:, None, None, None] # [B, 1, 1, 1]; share time across pixels.
|
| 76 |
-
xt = (1.0 - t_image) * x0 + t_image * x1 # Noise at t=0; real image at t=1.
|
| 77 |
-
ut = x1 - x0 # Derivative of the straight interpolation.
|
| 78 |
-
return xt, t, ut # Network input, time, and target velocity.
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
# %% 05. Define a simple convolution block
|
| 82 |
-
def conv_block(in_channels, out_channels):
|
| 83 |
-
return nn.Sequential(
|
| 84 |
-
nn.Conv2d(in_channels, out_channels, 3, padding=1), nn.SiLU(), # Keep H and W.
|
| 85 |
-
nn.Conv2d(out_channels, out_channels, 3, padding=1), nn.SiLU(), # Refine features.
|
| 86 |
-
)
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
# %% 06. Define the small U-Net
|
| 90 |
-
class FlowUNet(nn.Module):
|
| 91 |
-
def __init__(self, base_channels=BASE_CHANNELS):
|
| 92 |
-
super().__init__()
|
| 93 |
-
c = base_channels # Base width; c=32 by default.
|
| 94 |
-
self.encoder1 = conv_block(2, c) # Image + time -> c channels.
|
| 95 |
-
self.encoder2 = conv_block(c, 2 * c) # Encoder: c -> 2c.
|
| 96 |
-
self.middle = conv_block(2 * c, 4 * c) # Bottleneck: 2c -> 4c.
|
| 97 |
-
self.decoder2 = conv_block(6 * c, 2 * c) # Upsampled 4c + skip 2c -> 2c.
|
| 98 |
-
self.decoder1 = conv_block(3 * c, c) # Upsampled 2c + skip c -> c.
|
| 99 |
-
self.output = nn.Conv2d(c, 1, 1) # One signed velocity per pixel.
|
| 100 |
-
self.pool = nn.MaxPool2d(2) # Halve the image height and width.
|
| 101 |
-
|
| 102 |
-
# %% 07. Compute the velocity field
|
| 103 |
-
def forward(self, xt, t):
|
| 104 |
-
t_image = t[:, None, None, None].expand_as(xt) # Constant time channel.
|
| 105 |
-
x = torch.cat([xt, t_image], dim=1) # [B, 2, 28, 28].
|
| 106 |
-
skip1 = self.encoder1(x) # Keep full-resolution features.
|
| 107 |
-
skip2 = self.encoder2(self.pool(skip1)) # 28x28 -> 14x14.
|
| 108 |
-
x = self.middle(self.pool(skip2)) # 14x14 -> 7x7.
|
| 109 |
-
x = F.interpolate(x, size=skip2.shape[-2:], mode="nearest") # 7x7 -> 14x14.
|
| 110 |
-
x = self.decoder2(torch.cat([x, skip2], dim=1)) # Restore encoder detail.
|
| 111 |
-
x = F.interpolate(x, size=skip1.shape[-2:], mode="nearest") # 14x14 -> 28x28.
|
| 112 |
-
x = self.decoder1(torch.cat([x, skip1], dim=1)) # Restore fine detail.
|
| 113 |
-
v_pred = self.output(x) # Predict v_theta(xt, t).
|
| 114 |
-
return v_pred # [B, 1, 28, 28], matching xt.
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
# %% 08. Train for one epoch
|
| 118 |
-
def train_one_epoch(model, loader, optimizer, max_steps=0):
|
| 119 |
-
model.train() # Select training mode.
|
| 120 |
-
total_loss, num_images = 0.0, 0 # Accumulate an image-weighted epoch loss.
|
| 121 |
-
for step, (x1, _) in enumerate(loader): # Ignore digit labels: unconditional generation.
|
| 122 |
-
if max_steps and step >= max_steps:
|
| 123 |
-
break # The step budget the caller left for this epoch is spent.
|
| 124 |
-
x1 = x1.to(DEVICE) # Move real images to the model device.
|
| 125 |
-
xt, t, ut = sample_conditional_path(x1) # Fresh noise and times for this batch.
|
| 126 |
-
v_pred = model(xt, t) # The model receives only xt and t.
|
| 127 |
-
squared_error = (v_pred - ut).square() # Velocity error at every pixel.
|
| 128 |
-
loss = squared_error.flatten(1).sum(1).mean() # Sum pixels, average images.
|
| 129 |
-
optimizer.zero_grad(set_to_none=True) # Clear old gradients.
|
| 130 |
-
loss.backward() # Compute gradients with respect to weights.
|
| 131 |
-
optimizer.step() # Update the U-Net weights.
|
| 132 |
-
total_loss += loss.item() * x1.shape[0] # Weight by the actual batch size.
|
| 133 |
-
num_images += x1.shape[0] # Include the smaller final batch.
|
| 134 |
-
return total_loss / num_images # Average training loss per image.
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
# %% 09. Generate images with Euler integration
|
| 138 |
-
# Snapshots land every quarter of the default 100-step run, and the ODE states stay unclipped.
|
| 139 |
-
@torch.no_grad() # Sampling needs no gradient tracking.
|
| 140 |
-
def sample_images(model, num_samples=NUM_SAMPLES, steps=SAMPLE_STEPS):
|
| 141 |
-
if num_samples < 1 or steps < 1:
|
| 142 |
-
raise ValueError("num_samples and steps must be positive")
|
| 143 |
-
model.eval() # Select evaluation mode.
|
| 144 |
-
device = next(model.parameters()).device # Use the model device.
|
| 145 |
-
xt = torch.randn(num_samples, 1, 28, 28, device=device) # Initial x0.
|
| 146 |
-
dt = 1.0 / steps # Positive time increment.
|
| 147 |
-
snapshots = [xt.cpu().clone()] # Keep the initial noise for plotting.
|
| 148 |
-
for step in range(steps): # Integrate forward in time.
|
| 149 |
-
t = torch.full((num_samples,), step * dt, device=device) # Shared time grid.
|
| 150 |
-
v_pred = model(xt, t) # Reevaluate velocity at the current state.
|
| 151 |
-
xt = xt + dt * v_pred # Euler: state += time increment * velocity.
|
| 152 |
-
if (step + 1) % max(steps // 4, 1) == 0 or step + 1 == steps:
|
| 153 |
-
snapshots.append(xt.cpu().clone()) # Save intermediate and final states.
|
| 154 |
-
return xt, torch.stack(snapshots, dim=1) # Raw images; [B, snapshots, 1, 28, 28].
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
# %% 10. Set up the complete run
|
| 158 |
-
def main():
|
| 159 |
-
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
| 160 |
-
parser.add_argument("--epochs", type=int, default=EPOCHS)
|
| 161 |
-
parser.add_argument(
|
| 162 |
-
"--steps", type=int, default=0,
|
| 163 |
-
help="optimizer steps in the whole run; 0 runs every requested epoch in full",
|
| 164 |
-
)
|
| 165 |
-
parser.add_argument("--batch-size", type=int, default=BATCH_SIZE)
|
| 166 |
-
parser.add_argument("--dataset", choices=["mnist", "toy"], default="mnist")
|
| 167 |
-
parser.add_argument("--sample-steps", type=int, default=SAMPLE_STEPS)
|
| 168 |
-
parser.add_argument("--samples", type=int, default=NUM_SAMPLES)
|
| 169 |
-
parser.add_argument("--seed", type=int, default=SEED)
|
| 170 |
-
parser.add_argument("--out", type=str, default=str(OUTPUT_DIR))
|
| 171 |
-
parser.add_argument("--quiet", action="store_true", help="print only the final report")
|
| 172 |
-
args = parser.parse_args()
|
| 173 |
-
|
| 174 |
-
torch.manual_seed(args.seed) # Seed training randomness.
|
| 175 |
-
out = Path(args.out)
|
| 176 |
-
out.mkdir(parents=True, exist_ok=True) # Create the output folder.
|
| 177 |
-
loader = make_loader(args.dataset, args.batch_size) # Download, normalize, and batch.
|
| 178 |
-
model = FlowUNet().to(DEVICE) # Create the velocity network.
|
| 179 |
-
optimizer = torch.optim.AdamW(
|
| 180 |
-
model.parameters(), lr=LEARNING_RATE, weight_decay=0.0, # Optimize the flow loss.
|
| 181 |
-
)
|
| 182 |
-
num_parameters = sum(p.numel() for p in model.parameters()) # Count weights and biases.
|
| 183 |
-
if not args.quiet:
|
| 184 |
-
print(f"device={DEVICE} dataset={args.dataset} parameters={num_parameters:,}")
|
| 185 |
-
|
| 186 |
-
# %% 11. Train and save the learned parameters
|
| 187 |
-
losses = [] # Record the training loss after each epoch.
|
| 188 |
-
budget = args.steps # Optimizer steps left in the run, counted across all epochs.
|
| 189 |
-
for epoch in range(1, args.epochs + 1): # Complete the requested passes through the data.
|
| 190 |
-
loss = train_one_epoch(model, loader, optimizer, budget) # Run the training loop.
|
| 191 |
-
losses.append(loss) # Keep the loss history.
|
| 192 |
-
if not args.quiet:
|
| 193 |
-
print(f"epoch={epoch:02d} squared_norm_loss={loss:.3f}")
|
| 194 |
-
if args.steps: # An epoch runs the smaller of the budget and the number of batches.
|
| 195 |
-
budget -= min(budget, len(loader)) # Optimizer steps this epoch used.
|
| 196 |
-
if budget <= 0:
|
| 197 |
-
break # The run's budget is spent, so the remaining epochs do not run.
|
| 198 |
-
torch.save({
|
| 199 |
-
"model": model.state_dict(), "losses": losses, # Weights and training curve.
|
| 200 |
-
"base_channels": BASE_CHANNELS, # Width needed to reconstruct the model.
|
| 201 |
-
"seed": args.seed, # Record the training seed.
|
| 202 |
-
"epochs_completed": len(losses), # How far this run got.
|
| 203 |
-
}, out / "flow_unet_mnist.pt")
|
| 204 |
-
|
| 205 |
-
# %% 12. Sample and save images
|
| 206 |
-
torch.manual_seed(args.seed + 1) # Fix the starting noise for generation.
|
| 207 |
-
samples, trajectory = sample_images(model, args.samples, args.sample_steps)
|
| 208 |
-
try:
|
| 209 |
-
from torchvision.utils import save_image # Only needed to write the PNGs.
|
| 210 |
-
|
| 211 |
-
display_samples = ((samples.cpu() + 1.0) / 2.0).clamp(0.0, 1.0) # Clip for display only.
|
| 212 |
-
save_image(display_samples, out / "samples.png", nrow=4) # Final images.
|
| 213 |
-
display_path = ((trajectory[:4] + 1.0) / 2.0).clamp(0.0, 1.0) # First four sample paths.
|
| 214 |
-
save_image(
|
| 215 |
-
display_path.flatten(0, 1), out / "trajectory.png", # One row per sample.
|
| 216 |
-
nrow=trajectory.shape[1], # Time increases across columns.
|
| 217 |
-
)
|
| 218 |
-
if not args.quiet:
|
| 219 |
-
print(f"wrote {out}/samples.png and {out}/trajectory.png")
|
| 220 |
-
except ImportError: # Without torchvision the run still trains and samples.
|
| 221 |
-
if not args.quiet:
|
| 222 |
-
print("torchvision is not installed, so no PNG was written")
|
| 223 |
-
|
| 224 |
-
print(f"final loss {losses[-1]:.3f} sample range "
|
| 225 |
-
f"[{float(samples.min()):.2f}, {float(samples.max()):.2f}]")
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
# %% 13. Run the script
|
| 229 |
-
if __name__ == "__main__": # Execute when launched directly.
|
| 230 |
-
main() # Load data, train, sample, and save.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_2/interpolants.py
DELETED
|
@@ -1,210 +0,0 @@
|
|
| 1 |
-
"""Three interpolants between the prior and the data.
|
| 2 |
-
|
| 3 |
-
Replacing the weights 1 - t and t by any pair of schedules alpha_t and beta_t with alpha_0 = 1,
|
| 4 |
-
beta_0 = 0, alpha_1 = 0 and beta_1 = 1 leaves the conditional flow-matching argument intact, and
|
| 5 |
-
the regression target becomes the derivative alpha_dot X_0 + beta_dot X_1. The straight line, the
|
| 6 |
-
t-squared schedule and the trigonometric schedule differ in where the path goes, how fast it gets
|
| 7 |
-
there, and what the intermediate densities look like, so the same network trained on each produces
|
| 8 |
-
different samples.
|
| 9 |
-
|
| 10 |
-
Chapter 2, Section 2.18 of the notes. Trained on ``cis6270.data.gaussian_mixture_2d``. Prints the
|
| 11 |
-
chapter's table of positions and velocities at t = 0.25, the endpoint check for each schedule, the
|
| 12 |
-
identity covariance of the trigonometric schedule, and the samples each trained field produces at a
|
| 13 |
-
fine and at a coarse step budget.
|
| 14 |
-
|
| 15 |
-
python lecture_2/interpolants.py # train three fields, about fifteen seconds
|
| 16 |
-
python lecture_2/interpolants.py --steps 20 # a first look, with no time to learn
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
# %% 01. Imports and configuration
|
| 20 |
-
import argparse
|
| 21 |
-
import math
|
| 22 |
-
|
| 23 |
-
import torch
|
| 24 |
-
|
| 25 |
-
from cis6270.data import gaussian_mixture_2d
|
| 26 |
-
from cis6270.metrics import mean_log_density, mode_report
|
| 27 |
-
from cis6270.nets import VelocityMLP
|
| 28 |
-
from cis6270.runner import print_report, save_run, set_seed, train
|
| 29 |
-
|
| 30 |
-
DIM = 2 # d, the width of the state.
|
| 31 |
-
HALF_PI = math.pi / 2.0 # The constant in the trigonometric schedule and in its derivative.
|
| 32 |
-
TRAIN_STEPS = 1000 # Optimizer steps for each of the three fields.
|
| 33 |
-
BATCH_SIZE = 256 # Endpoint pairs per step.
|
| 34 |
-
LEARNING_RATE = 3e-3 # Optimizer step size.
|
| 35 |
-
WIDTH = 128 # Hidden width of the velocity network.
|
| 36 |
-
SAMPLE_STEPS = 64 # Euler steps in the fine sampling budget.
|
| 37 |
-
COARSE_STEPS = 8 # Euler steps in the coarse budget, where a varying velocity costs accuracy.
|
| 38 |
-
NUM_SAMPLES = 1024 # Points generated per schedule and per budget.
|
| 39 |
-
DATA_POINTS = 4096 # Size of the training set drawn from the four-Gaussian target.
|
| 40 |
-
COVARIANCE_POINTS = 8192 # Endpoint pairs used for the empirical covariance of X_t.
|
| 41 |
-
GRID = torch.tensor([0.0, 0.25, 0.50, 0.75, 1.0]) # [5] the chapter's grid of times.
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
# %% 02. The three schedules
|
| 45 |
-
#
|
| 46 |
-
# Section 2.18. Each returns alpha_t, beta_t and their time derivatives, evaluated on a batch of
|
| 47 |
-
# times. The pair of schedules fixes the path and the pair of derivatives fixes the target.
|
| 48 |
-
def linear(t):
|
| 49 |
-
"""alpha_t = 1 - t and beta_t = t, the straight segment traversed at constant speed."""
|
| 50 |
-
return 1.0 - t, t, -torch.ones_like(t), torch.ones_like(t) # Four tensors of shape [B].
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def quadratic(t):
|
| 54 |
-
"""alpha_t = 1 - t^2 and beta_t = t^2, the same segment entered slowly and left quickly."""
|
| 55 |
-
return 1.0 - t ** 2, t ** 2, -2.0 * t, 2.0 * t # U_t = 2t (X_1 - X_0), zero speed at t = 0.
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def trigonometric(t):
|
| 59 |
-
"""alpha_t = cos(pi t / 2) and beta_t = sin(pi t / 2), a quarter circle in the coefficients."""
|
| 60 |
-
angle = HALF_PI * t # [B] the angle the coefficient pair has turned through.
|
| 61 |
-
return (torch.cos(angle), torch.sin(angle),
|
| 62 |
-
-HALF_PI * torch.sin(angle), HALF_PI * torch.cos(angle))
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
SCHEDULES = {"linear": linear, "quadratic": quadratic, "trigonometric": trigonometric}
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def interpolate(schedule, x_0, x_1, t):
|
| 69 |
-
"""X_t = alpha_t X_0 + beta_t X_1 and U_t = alpha_dot X_0 + beta_dot X_1. Two [B, d] tensors."""
|
| 70 |
-
alpha, beta, alpha_dot, beta_dot = schedule(t) # [B] each, the two weights and their rates.
|
| 71 |
-
x_t = alpha[:, None] * x_0 + beta[:, None] * x_1 # [B, d] the state on the conditional path.
|
| 72 |
-
u_t = alpha_dot[:, None] * x_0 + beta_dot[:, None] * x_1 # [B, d] the regression target.
|
| 73 |
-
return x_t, u_t
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
# %% 03. The endpoints and the velocity at one time
|
| 77 |
-
#
|
| 78 |
-
# Section 2.18. The two ends are correct when alpha_0 = 1, beta_0 = 0, alpha_1 = 0 and beta_1 = 1,
|
| 79 |
-
# and Table 2.1 reads the three paths at X_0 = (2, 0), X_1 = (0, 2) and t = 0.25.
|
| 80 |
-
def endpoint_and_table(name):
|
| 81 |
-
"""The endpoint error of one schedule and its position and velocity at the chapter's input."""
|
| 82 |
-
x_0 = torch.tensor([[2.0, 0.0]]) # [1, d] the prior endpoint of the table.
|
| 83 |
-
x_1 = torch.tensor([[0.0, 2.0]]) # [1, d] the data endpoint of the table.
|
| 84 |
-
schedule = SCHEDULES[name]
|
| 85 |
-
ends, _ = interpolate(schedule, x_0.expand(2, DIM), x_1.expand(2, DIM),
|
| 86 |
-
torch.tensor([0.0, 1.0])) # [2, d] the states at t = 0 and t = 1.
|
| 87 |
-
start_error = (ends[0] - x_0[0]).abs().max() # X_0 is returned at t = 0.
|
| 88 |
-
finish_error = (ends[1] - x_1[0]).abs().max() # X_1 is returned at t = 1.
|
| 89 |
-
x_t, u_t = interpolate(schedule, x_0, x_1, torch.tensor([0.25])) # One quarter of the interval.
|
| 90 |
-
return {
|
| 91 |
-
f"{name}_endpoint_error": float(torch.maximum(start_error, finish_error)),
|
| 92 |
-
f"{name}_position_at_quarter": [round(float(v), 4) + 0.0 for v in x_t[0]],
|
| 93 |
-
f"{name}_velocity_at_quarter": [round(float(v), 4) + 0.0 for v in u_t[0]],
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
# %% 04. Identity covariance
|
| 98 |
-
#
|
| 99 |
-
# Section 2.18. For independent, centered endpoints with identity covariance the interpolation has
|
| 100 |
-
# covariance (alpha_t^2 + beta_t^2) I. That factor is cos^2 + sin^2 = 1 at every time for the
|
| 101 |
-
# trigonometric schedule, and below one between the ends for the other two.
|
| 102 |
-
def covariance_check(name, num_points, seed):
|
| 103 |
-
"""The coefficient squares on the grid, and the largest deviation of Cov(X_t) from the identity."""
|
| 104 |
-
set_seed(seed) # Same draw for every schedule, so the comparison is between schedules alone.
|
| 105 |
-
x_0 = torch.randn(num_points, DIM) # [N, d] a centered, white prior endpoint.
|
| 106 |
-
x_1 = torch.randn(num_points, DIM) # [N, d] a centered, white data endpoint, independent of it.
|
| 107 |
-
identity = torch.eye(DIM) # [d, d] the covariance the chapter's hypothesis gives each endpoint.
|
| 108 |
-
squares, deviations = [], []
|
| 109 |
-
for t in GRID: # One time at a time, since the covariance is measured over the batch.
|
| 110 |
-
times = t.expand(num_points) # [N] the same time for every pair.
|
| 111 |
-
alpha, beta, _, _ = SCHEDULES[name](times)
|
| 112 |
-
squares.append(round(float(alpha[0] ** 2 + beta[0] ** 2), 4)) # The exact variance factor.
|
| 113 |
-
x_t, _ = interpolate(SCHEDULES[name], x_0, x_1, times) # [N, d] the interpolated states.
|
| 114 |
-
covariance = (x_t.T @ x_t) / num_points # [d, d] the empirical second moment, centered at 0.
|
| 115 |
-
deviations.append(float((covariance - identity).abs().max()))
|
| 116 |
-
return {
|
| 117 |
-
f"{name}_coefficient_squares": squares,
|
| 118 |
-
f"{name}_covariance_deviation": max(deviations),
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
# %% 05. Training one field per interpolant
|
| 123 |
-
#
|
| 124 |
-
# Section 2.18 and Section 2.19. The loop is the one of the straight-line case with the target
|
| 125 |
-
# replaced, so any difference in the samples comes from the schedule and not from the setup.
|
| 126 |
-
def train_one(name, data, args):
|
| 127 |
-
"""Train v_theta against the conditional velocity of one schedule and return the loss history."""
|
| 128 |
-
set_seed(args.seed) # The same initialization and the same batches for every schedule.
|
| 129 |
-
model = VelocityMLP(dim=DIM, width=args.width) # v_theta(x, t).
|
| 130 |
-
schedule = SCHEDULES[name]
|
| 131 |
-
|
| 132 |
-
def loss_fn(step):
|
| 133 |
-
index = torch.randint(0, len(data), (args.batch_size,)) # [B] which data point to pair.
|
| 134 |
-
x_1 = data[index] # [B, d] the data endpoint.
|
| 135 |
-
x_0 = torch.randn_like(x_1) # [B, d] the prior endpoint, independent of it.
|
| 136 |
-
t = torch.rand(args.batch_size) # [B] t ~ Uniform(0, 1).
|
| 137 |
-
x_t, u_t = interpolate(schedule, x_0, x_1, t) # [B, d] the state and the target.
|
| 138 |
-
return (model(x_t, t) - u_t).square().sum(1).mean() # ||v_theta(X_t, t) - U_t||^2.
|
| 139 |
-
|
| 140 |
-
losses = train(model, loss_fn, steps=args.steps, lr=args.lr, quiet=args.quiet)
|
| 141 |
-
return model, losses
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
# %% 06. Euler sampling
|
| 145 |
-
#
|
| 146 |
-
# Section 2.19. Every schedule is sampled by the same integrator, so the step budget is the only
|
| 147 |
-
# thing a reader has to hold fixed when comparing them.
|
| 148 |
-
@torch.no_grad()
|
| 149 |
-
def sample(model, num_samples, steps):
|
| 150 |
-
"""Integrate dX_t/dt = v_theta(X_t, t) from t = 0 to t = 1 on a uniform grid. Returns [N, d]."""
|
| 151 |
-
x_t = torch.randn(num_samples, DIM) # [N, d] the draw X_0 ~ p_0.
|
| 152 |
-
dt = 1.0 / steps # Delta t, the width of one interval.
|
| 153 |
-
for step in range(steps): # One network evaluation per step.
|
| 154 |
-
x_t = x_t + dt * model(x_t, torch.full((num_samples,), step * dt)) # One Euler step.
|
| 155 |
-
return x_t
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
def measure(model, args):
|
| 159 |
-
"""Mode coverage and mean exact log density at the fine and the coarse step budget."""
|
| 160 |
-
set_seed(args.seed + 1) # The same starting noise for every schedule.
|
| 161 |
-
fine = sample(model, args.samples, args.sample_steps) # [N, d] points from the fine grid.
|
| 162 |
-
coarse = sample(model, args.samples, args.coarse_steps) # [N, d] points from the coarse grid.
|
| 163 |
-
report = mode_report(fine)
|
| 164 |
-
return {
|
| 165 |
-
"modes_found": report["modes_found"],
|
| 166 |
-
"mode_entropy": report["mode_entropy"],
|
| 167 |
-
"nearest_distance": report["nearest_distance"],
|
| 168 |
-
"mean_log_density": mean_log_density(fine),
|
| 169 |
-
"coarse_log_density": mean_log_density(coarse),
|
| 170 |
-
}
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
# %% 07. The report
|
| 174 |
-
def main():
|
| 175 |
-
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
| 176 |
-
parser.add_argument("--steps", type=int, default=TRAIN_STEPS, help="optimizer steps per field")
|
| 177 |
-
parser.add_argument("--batch-size", type=int, default=BATCH_SIZE, help="pairs per step")
|
| 178 |
-
parser.add_argument("--lr", type=float, default=LEARNING_RATE, help="learning rate")
|
| 179 |
-
parser.add_argument("--width", type=int, default=WIDTH, help="hidden width of the network")
|
| 180 |
-
parser.add_argument("--samples", type=int, default=NUM_SAMPLES, help="points to generate")
|
| 181 |
-
parser.add_argument("--sample-steps", type=int, default=SAMPLE_STEPS, help="fine Euler steps")
|
| 182 |
-
parser.add_argument("--coarse-steps", type=int, default=COARSE_STEPS, help="coarse Euler steps")
|
| 183 |
-
parser.add_argument("--seed", type=int, default=0, help="random seed")
|
| 184 |
-
parser.add_argument("--out", type=str, default="", help="directory for the run's output")
|
| 185 |
-
parser.add_argument("--quiet", action="store_true", help="print only the final report")
|
| 186 |
-
args = parser.parse_args()
|
| 187 |
-
|
| 188 |
-
data = gaussian_mixture_2d(DATA_POINTS, seed=args.seed) # [N, d] the target p_1 = p_data.
|
| 189 |
-
report = {}
|
| 190 |
-
for name in SCHEDULES: # Linear, quadratic, trigonometric, in the order the chapter gives them.
|
| 191 |
-
report.update(endpoint_and_table(name)) # Section 2.18, before any training.
|
| 192 |
-
report.update(covariance_check(name, COVARIANCE_POINTS, args.seed))
|
| 193 |
-
if not args.quiet:
|
| 194 |
-
print(f"training the {name} schedule")
|
| 195 |
-
model, losses = train_one(name, data, args)
|
| 196 |
-
report[f"{name}_final_loss"] = sum(losses[-100:]) / len(losses[-100:])
|
| 197 |
-
# The three losses are not comparable with each other, since each regresses on its own
|
| 198 |
-
# target, and the three measurements below are.
|
| 199 |
-
for key, value in measure(model, args).items():
|
| 200 |
-
report[f"{name}_{key}"] = value
|
| 201 |
-
save_run(f"{args.out}/{name}" if args.out else "", model=model, losses=losses)
|
| 202 |
-
|
| 203 |
-
report["data_mean_log_density"] = mean_log_density(data) # The same measurement on real points.
|
| 204 |
-
print_report("three interpolants on four Gaussians", report)
|
| 205 |
-
save_run(args.out, config=vars(args), report=report)
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
# %% 08. Run the script
|
| 209 |
-
if __name__ == "__main__":
|
| 210 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_2/velocity_fields.py
DELETED
|
@@ -1,263 +0,0 @@
|
|
| 1 |
-
"""Velocity fields, flow maps and the continuity equation.
|
| 2 |
-
|
| 3 |
-
A velocity field assigns one vector to every state and every time, and following those vectors from
|
| 4 |
-
a starting point traces a trajectory. The trajectories from every starting point together form the
|
| 5 |
-
flow map, and the continuity equation relates the motion of the density to the same field. Nothing
|
| 6 |
-
here is learned, so every number printed has a closed form beside it.
|
| 7 |
-
|
| 8 |
-
Chapter 2, Sections 2.2 to 2.8 of the notes. Runs on the chapter's analytic fields in one and two
|
| 9 |
-
dimensions. Prints the trajectories, the Euler error at one, two and four steps and the gap closing
|
| 10 |
-
like 1/N, the pushforward of a Gaussian prior, the flux balance on [-1, 1], both sides of the
|
| 11 |
-
continuity equation at x = 2, and the log density along one trajectory.
|
| 12 |
-
|
| 13 |
-
python lecture_2/velocity_fields.py # the full report
|
| 14 |
-
python lecture_2/velocity_fields.py --steps 20 # a shorter Euler refinement ladder
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
# %% 01. Imports and configuration
|
| 18 |
-
import argparse
|
| 19 |
-
import math
|
| 20 |
-
|
| 21 |
-
import torch
|
| 22 |
-
|
| 23 |
-
from cis6270.runner import print_report, save_run, set_seed
|
| 24 |
-
|
| 25 |
-
torch.set_default_dtype(torch.float64) # Six printed digits of the Euler ladder need it.
|
| 26 |
-
|
| 27 |
-
LOG2 = math.log(2.0) # The rate a of the expansion field v_t(x) = a x, so that e^{at} = 2^t.
|
| 28 |
-
GRID = torch.tensor([0.0, 0.25, 0.50, 0.75, 1.0]) # [5] the chapter's grid of times.
|
| 29 |
-
START_POINTS = torch.tensor([-1.0, 0.0, 1.0]) # [3] the initial states followed through each field.
|
| 30 |
-
EULER_STEPS = 64 # Largest step count in the refinement ladder, halved down to one step.
|
| 31 |
-
NUM_SAMPLES = 4096 # Draws from the prior p_0 used for the pushforward moments.
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
# %% 02. Three velocity fields and the trajectories they produce
|
| 35 |
-
#
|
| 36 |
-
# Section 2.2 and Section 2.3. Each field is a rule at a single instant, written as a function of
|
| 37 |
-
# the state x and the time t, and each has an exact solution to compare a solver against.
|
| 38 |
-
def constant_field(x, t):
|
| 39 |
-
"""v_t(x) = 2, the field that translates every state at the same speed."""
|
| 40 |
-
return torch.full_like(x, 2.0) # Same shape as x; the value depends on neither x nor t.
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def linear_field(x, t, a=LOG2):
|
| 44 |
-
"""v_t(x) = a x, which expands away from the origin for a > 0 and contracts toward it for a < 0."""
|
| 45 |
-
return a * x # Speed grows with distance from the origin and vanishes at x = 0.
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def rotation_field(x, t):
|
| 49 |
-
"""v(x_1, x_2) = (-x_2, x_1), everywhere perpendicular to the position vector."""
|
| 50 |
-
return torch.stack([-x[..., 1], x[..., 0]], dim=-1) # [..., 2] same shape as x.
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def exact_linear(x_0, t, a=LOG2):
|
| 54 |
-
"""x_t = x_0 e^{at}, the solution of dx_t/dt = a x_t obtained by separating variables."""
|
| 55 |
-
return x_0 * torch.exp(a * t) # Broadcasts a grid of times against a set of starting points.
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def trajectories():
|
| 59 |
-
"""The chapter's worked trajectories for translation, expansion, contraction and rotation."""
|
| 60 |
-
speed = constant_field(torch.tensor(1.0), torch.tensor(0.0)) # The value 2, at any state.
|
| 61 |
-
translated = 1.0 + speed * GRID # [5] the integral form x_t = x_0 + 2t from x_0 = 1.
|
| 62 |
-
expanded = exact_linear(START_POINTS, torch.tensor(1.0)) # [3] states at t = 1, each doubled.
|
| 63 |
-
contracted = exact_linear(START_POINTS, torch.tensor(1.0), a=-LOG2) # [3] states each halved.
|
| 64 |
-
turns = torch.tensor([math.pi / 2, math.pi]) # [2] the quarter turn and the half turn.
|
| 65 |
-
circle = torch.stack([torch.cos(turns), torch.sin(turns)], dim=-1) # [2, 2] x_t = (cos t, sin t).
|
| 66 |
-
# The squared norm is conserved because the field is orthogonal to the position: 2 x . v_t(x) = 0.
|
| 67 |
-
off_axis = torch.tensor([0.3, 0.7]) # [2] a state away from the axes, to test the conservation.
|
| 68 |
-
norm_rate = 2.0 * (off_axis * rotation_field(off_axis, torch.tensor(0.0))).sum() # d||x||^2/dt.
|
| 69 |
-
return {
|
| 70 |
-
"constant_field_trajectory": [round(float(v), 4) for v in translated],
|
| 71 |
-
"expansion_x_half_from_two": float(exact_linear(torch.tensor(2.0), torch.tensor(0.5))),
|
| 72 |
-
"expansion_endpoints": [round(float(v), 4) + 0.0 for v in expanded],
|
| 73 |
-
"expansion_outer_spacing": float(expanded[2] - expanded[0]),
|
| 74 |
-
"contraction_endpoints": [round(float(v), 4) + 0.0 for v in contracted],
|
| 75 |
-
"contraction_outer_spacing": float(contracted[2] - contracted[0]),
|
| 76 |
-
"rotation_quarter_turn": [round(float(v), 4) + 0.0 for v in circle[0]],
|
| 77 |
-
"rotation_half_turn": [round(float(v), 4) + 0.0 for v in circle[1]],
|
| 78 |
-
"rotation_radius": float(circle.norm(dim=-1).mean()),
|
| 79 |
-
"rotation_squared_norm_rate": float(norm_rate),
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
# %% 03. The flow map
|
| 84 |
-
#
|
| 85 |
-
# Section 2.4. Solving the ODE from every starting point at once defines phi_t, which is the identity
|
| 86 |
-
# at t = 0 and, for the expansion field, multiplies the whole line by 2^t.
|
| 87 |
-
def flow_map(x_0, t, a=LOG2):
|
| 88 |
-
"""phi_t(x_0) = x_0 e^{at}, the accumulated transformation after following v_t for a time t."""
|
| 89 |
-
return exact_linear(x_0, t, a) # [N] the state each starting point has reached by time t.
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def flow_map_report():
|
| 93 |
-
"""The identity at t = 0, the Jacobian determinant at t = 1, and noncrossing trajectories."""
|
| 94 |
-
probe = torch.linspace(-3.0, 3.0, 101) # [101] states spread over the line.
|
| 95 |
-
identity_error = (flow_map(probe, torch.tensor(0.0)) - probe).abs().max() # phi_0 = id.
|
| 96 |
-
x_0 = torch.tensor(1.3, requires_grad=True) # A state at which to differentiate phi_1.
|
| 97 |
-
jacobian = torch.autograd.grad(flow_map(x_0, torch.tensor(1.0)), x_0)[0] # d phi_1 / d x_0 = 2.
|
| 98 |
-
# Proposition 2.1: distinct trajectories of a Lipschitz field never share a state at a shared time.
|
| 99 |
-
gaps = (flow_map(START_POINTS[2], GRID) - flow_map(START_POINTS[0], GRID)).abs() # [5].
|
| 100 |
-
return {
|
| 101 |
-
"flow_map_identity_error": float(identity_error),
|
| 102 |
-
"flow_map_jacobian_det": float(jacobian),
|
| 103 |
-
"trajectory_gap_minimum": float(gaps.min()),
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
# %% 04. Euler's method
|
| 108 |
-
#
|
| 109 |
-
# Section 2.5. Within one step the velocity stays fixed at its value at the start of the step, so
|
| 110 |
-
# on dx_t/dt = x_t, where the velocity grows with the state, every step undershoots.
|
| 111 |
-
def euler(field, x_0, steps):
|
| 112 |
-
"""hat x_{k+1} = hat x_k + Delta t v_{t_k}(hat x_k) on a uniform grid of ``steps`` intervals."""
|
| 113 |
-
x_hat = x_0.clone() # The running approximation, started from the exact initial state.
|
| 114 |
-
dt = 1.0 / steps # Delta t, the width of one interval of the grid.
|
| 115 |
-
for k in range(steps): # One velocity evaluation per step.
|
| 116 |
-
x_hat = x_hat + dt * field(x_hat, torch.tensor(k * dt)) # The Euler update at grid time t_k.
|
| 117 |
-
return x_hat
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
def euler_ladder(largest):
|
| 121 |
-
"""Euler on dx_t/dt = x_t from x_0 = 1, at every power of two up to ``largest`` steps."""
|
| 122 |
-
exact = math.e # The exact solution x_1 = e^1, the chapter's 2.7183.
|
| 123 |
-
counts, landings, errors = [], [], []
|
| 124 |
-
steps = 1
|
| 125 |
-
largest = max(largest, 4) # The chapter's three rungs, at one, two and four steps, always run.
|
| 126 |
-
while steps <= largest: # 1, 2, 4, 8, ... up to the requested largest step count.
|
| 127 |
-
landing = float(euler(lambda x, t: x, torch.tensor(1.0), steps)) # The field is v_t(x) = x.
|
| 128 |
-
counts.append(steps)
|
| 129 |
-
landings.append(round(landing, 6))
|
| 130 |
-
errors.append(round(exact - landing, 6))
|
| 131 |
-
steps *= 2
|
| 132 |
-
return {
|
| 133 |
-
"euler_step_counts": counts,
|
| 134 |
-
"euler_landings": landings,
|
| 135 |
-
"euler_errors": errors,
|
| 136 |
-
# A first-order method has error proportional to 1/N, so the error times N settles at e/2.
|
| 137 |
-
"euler_error_times_steps": [round(e * n, 4) for e, n in zip(errors, counts)],
|
| 138 |
-
"euler_error_halving_ratios": [round(errors[i] / errors[i + 1], 4)
|
| 139 |
-
for i in range(len(errors) - 1)],
|
| 140 |
-
"euler_relative_error_four_steps": float((exact - landings[2]) / exact),
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
# %% 05. A random starting point
|
| 145 |
-
#
|
| 146 |
-
# Section 2.6. The initial state becomes a random variable, each draw follows the same field, and
|
| 147 |
-
# the law at time t is the pushforward p_t = (phi_t)_# p_0.
|
| 148 |
-
def pushforward_moments(num_samples, seed):
|
| 149 |
-
"""Moments of phi_1(X_0) for X_0 ~ N(0, 1) under the expansion field, against N(0, 4)."""
|
| 150 |
-
set_seed(seed) # Same seed, same draw, same report.
|
| 151 |
-
x_0 = torch.randn(num_samples) # [N] draws from the prior p_0 = N(0, 1).
|
| 152 |
-
x_1 = flow_map(x_0, torch.tensor(1.0)) # [N] states at t = 1, every one of them doubled.
|
| 153 |
-
return {
|
| 154 |
-
"pushforward_mean": float(x_1.mean()), # Exact value 0, since the flow fixes the origin.
|
| 155 |
-
"pushforward_variance": float(x_1.var(unbiased=False)), # Scaling by 2 gives variance 4.
|
| 156 |
-
"pushforward_exact_variance": 4.0, # Scaling a centered law by c multiplies variance by c^2.
|
| 157 |
-
}
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
# %% 06. Probability flux and the continuity equation
|
| 161 |
-
#
|
| 162 |
-
# Section 2.7. The flux j_t(x) = p_t(x) v_t(x) measures probability transported per unit time, and
|
| 163 |
-
# the balance over an interval becomes the continuity equation once the interval shrinks to a point.
|
| 164 |
-
def spreading_density(x, t):
|
| 165 |
-
"""p_t(x) for X_t ~ N(0, 4^t), the law the expansion field produces from a standard normal."""
|
| 166 |
-
std = torch.pow(torch.tensor(2.0), t) # 2^t, the standard deviation at time t.
|
| 167 |
-
return torch.exp(-x ** 2 / (2.0 * std ** 2)) / (std * math.sqrt(2.0 * math.pi))
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
def flux_balance():
|
| 171 |
-
"""The two endpoint fluxes on [-1, 1] at t = 0, and the rate of change of the enclosed mass."""
|
| 172 |
-
t_0 = torch.tensor(0.0)
|
| 173 |
-
endpoints = torch.tensor([-1.0, 1.0]) # [2] the interval's endpoints a and b.
|
| 174 |
-
density = spreading_density(endpoints, t_0) # [2] p_0(-1) and p_0(1), both 0.2420.
|
| 175 |
-
flux = density * linear_field(endpoints, t_0) # [2] j_0(x) = p_0(x) v_0(x).
|
| 176 |
-
# The same rate read off the distribution: M_t = erf(2^{-t} / sqrt 2), differentiated in t.
|
| 177 |
-
t = torch.tensor(0.0, requires_grad=True)
|
| 178 |
-
mass = torch.erf(torch.pow(torch.tensor(2.0), -t) / math.sqrt(2.0)) # P(|X_t| <= 1).
|
| 179 |
-
mass_rate = torch.autograd.grad(mass, t)[0] # d M_t / dt at t = 0.
|
| 180 |
-
return {
|
| 181 |
-
"density_at_one": float(density[1]),
|
| 182 |
-
"flux_at_minus_one": float(flux[0]),
|
| 183 |
-
"flux_at_one": float(flux[1]),
|
| 184 |
-
"flux_balance": float(flux[0] - flux[1]), # j_0(a) - j_0(b), the net rate into [-1, 1].
|
| 185 |
-
"enclosed_mass": float(mass.detach()),
|
| 186 |
-
"enclosed_mass_rate": float(mass_rate),
|
| 187 |
-
}
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
def continuity_check():
|
| 191 |
-
"""Both sides of the continuity equation at x = 2 and t = 0, in the expanded product-rule form."""
|
| 192 |
-
x = torch.tensor(2.0, requires_grad=True) # The position held fixed while the density changes.
|
| 193 |
-
t = torch.tensor(0.0, requires_grad=True) # The time at which both sides are evaluated.
|
| 194 |
-
p = spreading_density(x, t) # p_0(2), the chapter's 0.05399.
|
| 195 |
-
dp_dt = torch.autograd.grad(p, t, create_graph=True)[0] # Local time derivative of the density.
|
| 196 |
-
dp_dx = torch.autograd.grad(p, x, create_graph=True)[0] # Spatial gradient of the density.
|
| 197 |
-
divergence = torch.autograd.grad(linear_field(x, t), x)[0] # div v = log 2 in one dimension.
|
| 198 |
-
transport = linear_field(x, t) * dp_dx # v_t(x) . grad p_t(x), the transport term.
|
| 199 |
-
volume = p * divergence # p_t(x) div v_t(x), the local volume-change term.
|
| 200 |
-
return {
|
| 201 |
-
"density_at_two": float(p.detach()),
|
| 202 |
-
"density_gradient_at_two": float(dp_dx.detach()),
|
| 203 |
-
"velocity_divergence": float(divergence),
|
| 204 |
-
"continuity_time_derivative": float(dp_dt.detach()),
|
| 205 |
-
"continuity_minus_flux_divergence": float(-(transport + volume).detach()),
|
| 206 |
-
}
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
# %% 07. The density along a moving trajectory
|
| 210 |
-
#
|
| 211 |
-
# Section 2.8. An observer riding with the sample sees no transport term, so the log density changes
|
| 212 |
-
# at the rate -div v_t(x_t), and over a unit of time that agrees with the Jacobian of the flow map.
|
| 213 |
-
def log_density_along_trajectory(x_start=0.7):
|
| 214 |
-
"""The instantaneous change-of-variables equation on the expansion field, both ways."""
|
| 215 |
-
x_0 = torch.tensor(x_start) # One starting point, carried from t = 0 to t = 1.
|
| 216 |
-
x_1 = flow_map(x_0, torch.tensor(1.0)) # The state the trajectory reaches at t = 1.
|
| 217 |
-
log_p0 = torch.log(spreading_density(x_0, torch.tensor(0.0))) # log p_0(x_0).
|
| 218 |
-
log_p1 = torch.log(spreading_density(x_1, torch.tensor(1.0))) # log p_1(x_1).
|
| 219 |
-
origin = torch.tensor(0.0) # The fixed point of the flow, where phi_1(0) = 0.
|
| 220 |
-
prior_at_origin = float(spreading_density(origin, torch.tensor(0.0))) # p_0(0) = 0.3989.
|
| 221 |
-
final_at_origin = float(spreading_density(origin, torch.tensor(1.0))) # p_1(0) = 0.1995.
|
| 222 |
-
return {
|
| 223 |
-
"log_density_rate": -LOG2, # d log p_t(x_t) / dt = -div v_t(x_t), a constant here.
|
| 224 |
-
"log_density_change": float(log_p1 - log_p0), # Measured from the two densities.
|
| 225 |
-
"log_density_accumulated": -LOG2 * 1.0, # The integral of -div v_s over s from 0 to 1.
|
| 226 |
-
"prior_density_at_origin": prior_at_origin,
|
| 227 |
-
"final_density_at_origin": final_at_origin,
|
| 228 |
-
"density_ratio_at_origin": prior_at_origin / final_at_origin, # |det J_{phi_1}| = 2.
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
# %% 08. The report
|
| 233 |
-
def main():
|
| 234 |
-
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
|
| 235 |
-
parser.add_argument("--steps", type=int, default=EULER_STEPS, help="largest Euler step count")
|
| 236 |
-
parser.add_argument("--samples", type=int, default=NUM_SAMPLES, help="draws from the prior")
|
| 237 |
-
parser.add_argument("--seed", type=int, default=0, help="random seed")
|
| 238 |
-
parser.add_argument("--out", type=str, default="", help="directory for the run's report")
|
| 239 |
-
parser.add_argument("--quiet", action="store_true", help="print only the final report")
|
| 240 |
-
args = parser.parse_args()
|
| 241 |
-
|
| 242 |
-
report = {}
|
| 243 |
-
report.update(trajectories()) # Sections 2.2 and 2.3.
|
| 244 |
-
report.update(flow_map_report()) # Section 2.4.
|
| 245 |
-
report.update(euler_ladder(args.steps)) # Section 2.5.
|
| 246 |
-
report.update(pushforward_moments(args.samples, args.seed)) # Section 2.6.
|
| 247 |
-
report.update(flux_balance()) # Section 2.7.
|
| 248 |
-
report.update(continuity_check()) # Section 2.7.
|
| 249 |
-
report.update(log_density_along_trajectory()) # Section 2.8.
|
| 250 |
-
|
| 251 |
-
if not args.quiet: # The Euler ladder, rung by rung, as the chapter develops it.
|
| 252 |
-
for n, landing, error in zip(report["euler_step_counts"], report["euler_landings"],
|
| 253 |
-
report["euler_errors"]):
|
| 254 |
-
print(f" euler steps {n:3d} landing {landing:.6f} error {error:.6f}"
|
| 255 |
-
f" error x steps {error * n:.4f}")
|
| 256 |
-
|
| 257 |
-
print_report("velocity fields, flow maps and the continuity equation", report)
|
| 258 |
-
save_run(args.out, report=report) # Writes report.json when --out names a directory.
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
# %% 09. Run the script
|
| 262 |
-
if __name__ == "__main__":
|
| 263 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|