pranamanam commited on
Commit
2ac8892
·
verified ·
1 Parent(s): 44cbe70

Delete lecture_2

Browse files
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()