Delete lecture_6
Browse files- lecture_6/README.md +0 -150
- lecture_6/categorical_map.py +0 -244
- lecture_6/consistency.py +0 -231
- lecture_6/discrete_map.py +0 -218
- lecture_6/expanding.py +0 -217
- lecture_6/flow_map.py +0 -206
- lecture_6/fmlm.py +0 -291
- lecture_6/meanflow.py +0 -175
- lecture_6/posterior_map.py +0 -221
- lecture_6/residuals.py +0 -204
- lecture_6/shortcut.py +0 -184
- lecture_6/stochastic_map.py +0 -218
lecture_6/README.md
DELETED
|
@@ -1,150 +0,0 @@
|
|
| 1 |
-
# Lecture 6: Flow Maps
|
| 2 |
-
|
| 3 |
-
Chapter 6 of the notes, `book/chapters/ch6_flow_maps.tex`.
|
| 4 |
-
|
| 5 |
-
Chapters 2 to 5 learned a local change and applied it again and again, so the number of steps set
|
| 6 |
-
the cost of a sample. This lecture learns the map between two times directly. The map
|
| 7 |
-
`X_{s,t}(x)` is fixed by three axioms, identity, cocycle and tangent, and every file writes it the
|
| 8 |
-
same way, as the state plus the elapsed time times an average velocity the network produces, which
|
| 9 |
-
makes the identity axiom hold for any weights at all. Differentiating the cocycle in the end time
|
| 10 |
-
gives the Lagrangian identity, in the start time the Eulerian identity, and leaving it finite
|
| 11 |
-
gives the composition identity. Each one becomes a squared residual, each is paired with a flow
|
| 12 |
-
matching anchor on the diagonal, and the rest of the lecture carries the construction into five
|
| 13 |
-
settings: a point in space, a distribution over tokens, a Brownian path, a dimension that grows,
|
| 14 |
-
and a conditioning state.
|
| 15 |
-
|
| 16 |
-
Every file is standalone, runs offline on a laptop CPU, and prints the numbers the chapter works
|
| 17 |
-
by hand.
|
| 18 |
-
|
| 19 |
-
## The files
|
| 20 |
-
|
| 21 |
-
| File | Command | What it builds |
|
| 22 |
-
| --- | --- | --- |
|
| 23 |
-
| `flow_map.py` | `python lecture_6/flow_map.py` | No learning. The chapter's velocity, the map by RK4, the three axioms, the inverse, the Jacobian and its differential equation, Liouville's formula, density transport and the generator |
|
| 24 |
-
| `residuals.py` | `python lecture_6/residuals.py` | The Lagrangian, Eulerian and composition residuals and the diagonal anchor, from the chapter's listing, with four models trained on the four Gaussian blobs |
|
| 25 |
-
| `meanflow.py` | `python lecture_6/meanflow.py` | MeanFlow: the Eulerian identity in the average-velocity parameterization, with the total derivative from one forward-mode pass |
|
| 26 |
-
| `shortcut.py` | `python lecture_6/shortcut.py` | Shortcut models: the composition identity indexed by step size, on a dyadic ladder from 1/128 to 1 |
|
| 27 |
-
| `consistency.py` | `python lecture_6/consistency.py` | Consistency models on the edge `t = 1` with the boundary coefficients, and consistency trajectory models on the whole triangle |
|
| 28 |
-
| `fmlm.py` | `python lecture_6/fmlm.py` | Flow Map Language Models: the two-time denoiser, the convex form, the cocycle in denoiser coordinates, the two gradients a softmax head sees, two KL terms and the decoding clock, on DNA |
|
| 29 |
-
| `categorical_map.py` | `python lecture_6/categorical_map.py` | Categorical Flow Maps: the Lagrangian identity with a softmax head, the teacher gap and drift, and the bound that replaces the residual, on phrases |
|
| 30 |
-
| `discrete_map.py` | `python lecture_6/discrete_map.py` | Discrete Flow Maps: why the derivative targets leave the simplex, and the repair in logit space, on DNA |
|
| 31 |
-
| `stochastic_map.py` | `python lecture_6/stochastic_map.py` | Strong stochastic flow maps: the solution map of an SDE, the weak map's floor, the Legendre coefficients and the Chen relations |
|
| 32 |
-
| `expanding.py` | `python lecture_6/expanding.py` | Expanding flow maps: a state whose length grows, local clocks, the composition identity under growth, and one draw of the insertion noise |
|
| 33 |
-
| `posterior_map.py` | `python lecture_6/posterior_map.py` | Diamond Maps and Meta Flow Maps: the inner clock, posterior samples, the value function from K samples, its gradient and the importance weight |
|
| 34 |
-
|
| 35 |
-
Every file takes the shared flags of `cis6270.runner`. `--steps 20 --quiet` finishes in a few
|
| 36 |
-
seconds. In `flow_map.py`, `stochastic_map.py` and `posterior_map.py` nothing is trained, so
|
| 37 |
-
`--steps` sets the Runge-Kutta substep count, the Monte Carlo draw count and the inner substep
|
| 38 |
-
count instead, which each file's docstring says.
|
| 39 |
-
|
| 40 |
-
## What a default run prints
|
| 41 |
-
|
| 42 |
-
The chapter's worked numbers come out exactly, and the test suite asserts them.
|
| 43 |
-
|
| 44 |
-
| Quantity | Chapter | Printed |
|
| 45 |
-
| --- | --- | --- |
|
| 46 |
-
| `X_{0,1}(0.7)`, the cocycle through 0.5, the inverse, `det J_{0,1}` | 4.1, 4.1, 0.7, 3 | 4.100000, 4.100000, 0.700000, 3.000000 |
|
| 47 |
-
| `log det J_{0,1}` by Liouville, and the density ratio | 1.0986, 3.000 | 1.0986, 3.0000 |
|
| 48 |
-
| Stay-still residuals at `x = 1`, `s = 0`, `t = 1` | 1, 1, 0 | 1.0000, 1.0000, 0.0000 |
|
| 49 |
-
| MeanFlow on `dx/dt = x`: `1.718x = x + 0.718x` | 1.718, 1, 0.718 | 1.7183, 1.0000, 0.7183 |
|
| 50 |
-
| Shortcut doubling: two steps of 0.25 against one of 0.5 | 1.297443 | 1.297443 |
|
| 51 |
-
| Consistency distillation residual at `h = 0.01` | 8.189e-05 | 8.189e-05 |
|
| 52 |
-
| FMLM composed target, its four KL terms and their sum | (0.06, 0.12, 0.76, 0.06), 0.030747 | the same, 0.030747 |
|
| 53 |
-
| The two gradients in the logits at that target and student | (0.04, 0.03, -0.11, 0.04) and (0.0099, 0.01335, -0.03315, 0.0099) | the same |
|
| 54 |
-
| The same two once the student saturates to `q_1 = 1e-06` | -0.0600 and -6.10e-08 | -0.0600 and -4.6171e-08, see below |
|
| 55 |
-
| The decoding clock at 0.60, 0.75, 0.80, 0.85 | 0.006, 0.141, 0.456, 0.931 | 0.0057, 0.1411, 0.4561, 0.9313 |
|
| 56 |
-
| Four steps on the clock | 0.773490, 0.804481, 0.828095 | the same |
|
| 57 |
-
| Gamma on the last three times, and over the long first leg | 0.494620, 0.830144 | the same |
|
| 58 |
-
| Categorical gap and drift, and the bound around a zero residual | antiparallel, 0.199 | 1e-34 residual, 0.1989 |
|
| 59 |
-
| The same two terms averaged over three thousand states | 0.393, 0.095 | 0.3955, 0.0963 |
|
| 60 |
-
| Discrete derivative target, three entries negative | (-0.3333, -0.6667, 1.3333, -0.3333) | the same |
|
| 61 |
-
| The logit repair of (-0.284, 0.828, 0.456) | (0.0194, 0.6259, 0.3547) | the same |
|
| 62 |
-
| The weak map's floor `2K_1` | 6.389 | 6.3886 by Monte Carlo |
|
| 63 |
-
| Chen fusion of four half-interval draws | 0.50, -0.20 | 0.50, -0.20 |
|
| 64 |
-
| Local clocks at `t = 0.8`, and omega at (0.3, 0.6, 0.9) | 0.800, 0.692, 0.333 and 0.125 | the same |
|
| 65 |
-
| Jensen gap, and `E[V_K]` at K = 1, 2, 4, 8 | 0.434 and 1.000, 1.217, 1.340, 1.393 | the same |
|
| 66 |
-
| Meta Flow Maps fixed point against the per-sample ratio | 0.625 and 0.540 | the same |
|
| 67 |
-
|
| 68 |
-
One of those disagrees with the notes. Section 6.27.4 quotes a saturated squared-error gradient of
|
| 69 |
-
`-6.10e-08`. Its own expression, the gradient of half the squared error through the softmax
|
| 70 |
-
Jacobian,
|
| 71 |
-
|
| 72 |
-
d/dz_i (1/2) sum_j (q_j - p_j)^2 = q_i (q_i - p_i) - q_i sum_j q_j (q_j - p_j) ,
|
| 73 |
-
|
| 74 |
-
evaluated at `p = (0.06, 0.12, 0.76, 0.06)` and the student moved to `q_1 = 1e-06` with the other
|
| 75 |
-
three renormalized, gives `-4.6171e-08`. The first term on its own is `-6.00e-08`, so the quoted
|
| 76 |
-
number is that term with the second one dropped. `fmlm.py` prints the full expression, takes both
|
| 77 |
-
gradients by automatic differentiation, and reproduces the unsaturated pair of Section 6.27.4
|
| 78 |
-
exactly. The point the chapter draws from the number is unaffected, since `-0.0600` against
|
| 79 |
-
`-4.6171e-08` is still six orders of magnitude.
|
| 80 |
-
|
| 81 |
-
The trained numbers from one default run on a laptop CPU, with seed 0.
|
| 82 |
-
|
| 83 |
-
| Run | Headline |
|
| 84 |
-
| --- | --- |
|
| 85 |
-
| `residuals.py`, 600 steps each | distance to its own ODE 0.900 for the diagonal alone, 0.481, 0.376 and 0.541 with the Lagrangian, Eulerian and composition residuals; fraction on a mode 0.44 against 0.74, 0.66 and 0.70 |
|
| 86 |
-
| `meanflow.py`, 2000 steps | nearest mode 0.627 at one step, 0.522 at two, 0.505 at four, all four modes found |
|
| 87 |
-
| `shortcut.py`, 2000 steps | nearest mode 0.605 at one step and 0.508 at eight; the doubling residual rises from below 1e-04 at `d = 1/128` to 0.034 at `d = 1/2` |
|
| 88 |
-
| `consistency.py`, 1500 steps each | nearest mode 0.513 for the consistency model at one step, 0.515, 0.420 and 0.470 for the trajectory model at one, two and four |
|
| 89 |
-
| `fmlm.py`, 1500 steps | GC 0.586 at one step against 0.581 in the training data, motif 0.63, token entropy 1.371 of a possible 1.386, every sequence distinct |
|
| 90 |
-
| `categorical_map.py`, 1000 steps each | every phrase in the language at both step counts; trained on the residual, 0.56 of one-step phrases distinct at entropy 2.75; trained on the bound, 0.33 at 2.54, and 0.12 at 2.05 by four steps |
|
| 91 |
-
| `discrete_map.py`, 1500 steps | GC 0.609 at one step, motif 0.81, entropy 1.362, every sequence distinct; the unrepaired additive target is negative on 0.05% of its entries |
|
| 92 |
-
| `stochastic_map.py` | the strong map's terminal cloud sits 8.8e-04 from the finest crossing at two hops, 1.1e-04 at four, 1.4e-05 at eight and 2e-06 at sixteen, the weak map's 3.06 at every hop count |
|
| 93 |
-
| `expanding.py`, 1500 steps | GC 0.617 at one step, 0.621 on coordinates born at the source and 0.613 on coordinates born later; motif 0.75 |
|
| 94 |
-
| `posterior_map.py` | on the blobs, the exact value -0.1127, the 16-sample estimate -0.1088, the single endpoint -0.1628 |
|
| 95 |
-
|
| 96 |
-
The DNA files draw 1024 training sequences of the default length 16, which have GC fraction 0.581
|
| 97 |
-
and motif fraction 0.749. They read 64 samples, as the phrase file does, so a reported fraction
|
| 98 |
-
carries about 0.06 of sampling noise and its second digit moves between seeds.
|
| 99 |
-
|
| 100 |
-
Longer runs, where they change the picture:
|
| 101 |
-
|
| 102 |
-
- `residuals.py --steps 2400` moves the four distances to 1.033, 0.171, 0.247 and 0.511 and the
|
| 103 |
-
four mode fractions to 0.27, 0.82, 0.71 and 0.79, against Table 6.2's 0.896, 0.292, 0.242 and
|
| 104 |
-
0.325 at 0.12, 0.84, 0.82 and 0.84. The gap between the anchor alone and the anchor plus any
|
| 105 |
-
residual widens with the budget, and the three residuals stay inside each other's spread.
|
| 106 |
-
- `meanflow.py --steps 8000` brings the one-step nearest mode from 0.627 to 0.437, below its own
|
| 107 |
-
two-step and four-step figures, which is the one-step map overtaking the grid.
|
| 108 |
-
- `categorical_map.py --steps 3000` widens the gap between the two students, to 0.60 of one-step
|
| 109 |
-
phrases distinct for the residual against 0.27 for the bound, and 0.54 against 0.09 at four
|
| 110 |
-
steps. The bounded student loses variety as its budget grows, which is what Section 6.28.4
|
| 111 |
-
predicts, since the bound is minimized by a prediction that does not move with the end time.
|
| 112 |
-
- The DNA runs are at their plateau by their default budgets. At `--steps 6000` the one-step motif
|
| 113 |
-
fraction of `fmlm.py` reads 0.61 and of `discrete_map.py` 0.69, inside the sampling noise of the
|
| 114 |
-
default run.
|
| 115 |
-
|
| 116 |
-
Timings at the default flags, on one core. Three or four seconds for `flow_map.py`,
|
| 117 |
-
`stochastic_map.py` and `posterior_map.py`, which train nothing, and twelve to twenty-seven
|
| 118 |
-
seconds for the eight that do, including `residuals.py` with its four models and
|
| 119 |
-
`categorical_map.py` and `consistency.py` with their two.
|
| 120 |
-
|
| 121 |
-
## What is simplified
|
| 122 |
-
|
| 123 |
-
- The four blob models of `residuals.py` train for 600 steps on a four-layer network, so the
|
| 124 |
-
distances are larger than Table 6.2 of the chapter, which trains longer. The ordering the
|
| 125 |
-
chapter draws, the anchor alone against the anchor plus any residual, comes out at this budget.
|
| 126 |
-
- `shortcut.py` evaluates the zero-step term and the doubling term on the whole batch and weights
|
| 127 |
-
them three to one, in place of the chapter's split of the batch in that proportion. The two have
|
| 128 |
-
the same expectation and the weighted form costs one network call more per step.
|
| 129 |
-
- `consistency.py` distils from the exact marginal velocity of the training set, which is a
|
| 130 |
-
softmax over the data points, in place of a separately trained flow matching teacher. The
|
| 131 |
-
teacher's discretization error is then the only error the student inherits, which is the point
|
| 132 |
-
of Section 6.20.
|
| 133 |
-
- `posterior_map.py` builds the inner map from the exact posterior over the training points, in
|
| 134 |
-
place of training a conditional family, so the inner velocity is a closed form and the value,
|
| 135 |
-
its gradient and the Jensen gap are all available to compare against.
|
| 136 |
-
- The decoding clock of `fmlm.py` is computed by quadrature on the one-position integral, which is
|
| 137 |
-
exact for the Gaussian interpolant and says nothing about correlations between positions.
|
| 138 |
-
- `categorical_map.py` and `discrete_map.py` keep the end time below one, because the drift term
|
| 139 |
-
of each loss divides by the remaining time. `fmlm.py` has no such term, so it draws a quarter of
|
| 140 |
-
each batch at `t = 1`, the slice Section 6.26 asks for by name.
|
| 141 |
-
- `stochastic_map.py` carries three Legendre coefficients and fuses them with the Chen relations
|
| 142 |
-
written out for `n = 0, 1, 2`. A fourth coefficient needs the constants `c_{3,m}`, which the
|
| 143 |
-
chapter does not list.
|
| 144 |
-
- Every network is `cis6270.nets.TwoTimeMLP`. The discrete files flatten the one-hot lift into its
|
| 145 |
-
input and read its output as per-position logits, so one architecture covers both halves of the
|
| 146 |
-
lecture and any difference in the samples comes from the loss.
|
| 147 |
-
- Each file sets `torch.set_num_threads(1)`. These networks are small enough that splitting one
|
| 148 |
-
matrix product across cores costs more than it saves. `tests/conftest.py` sets the same thing
|
| 149 |
-
for the test session, so the line in each file is the one that acts when a reader runs the
|
| 150 |
-
script directly.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_6/categorical_map.py
DELETED
|
@@ -1,244 +0,0 @@
|
|
| 1 |
-
"""Categorical Flow Maps, the Lagrangian identity on the simplex and the bound that replaces it.
|
| 2 |
-
|
| 3 |
-
A softmax head makes the two-time prediction a distribution at every pair of times, and the
|
| 4 |
-
Lagrangian identity rewritten in those coordinates reads pi_{s,t} = pi_{t,t}(X_{s,t}) minus a
|
| 5 |
-
coefficient times the end-time derivative of the prediction. The derivative has no sign
|
| 6 |
-
constraint, so the residual splits into a teacher gap, which is a difference of two distributions,
|
| 7 |
-
and a drift, which is the scaled derivative. A parallelogram bound and Pinsker's inequality turn
|
| 8 |
-
the squared residual into four KL divergences plus two squared drifts, and at the exact map the
|
| 9 |
-
two pieces are antiparallel, so the bound is strictly positive where the residual is zero.
|
| 10 |
-
|
| 11 |
-
Chapter 6, Section 6.28 of the notes. Trained on the four-token phrases of
|
| 12 |
-
``cis6270.data.phrases``. Prints the chapter's worked cancellation on three tokens, where the gap
|
| 13 |
-
is the exact negative of the drift and the bound reads 0.199 around a residual of zero, and then
|
| 14 |
-
two models trained on the residual and on its bound.
|
| 15 |
-
|
| 16 |
-
python lecture_6/categorical_map.py # two models, 1000 steps each
|
| 17 |
-
python lecture_6/categorical_map.py --steps 20 # a few seconds, for a first look
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
# %% 01. Imports and configuration
|
| 21 |
-
import torch
|
| 22 |
-
|
| 23 |
-
from cis6270.data import phrases
|
| 24 |
-
from cis6270.metrics import token_entropy, unique_fraction
|
| 25 |
-
from cis6270.nets import TwoTimeMLP, count_parameters
|
| 26 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed, train
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
torch.set_num_threads(1) # These networks are small, and splitting one matrix product costs more
|
| 30 |
-
# than it saves. ``tests/conftest.py`` sets the same thing for the test session, so the line
|
| 31 |
-
# here is the one that acts when a reader runs this script directly.
|
| 32 |
-
|
| 33 |
-
BOOK_PRIOR = (0.5, 0.3, 0.2) # The three data probabilities of the chapter's worked example.
|
| 34 |
-
BOOK_SOURCE = (-0.5739, -0.8344, -0.0584) # The source draw I_0 the chapter starts from.
|
| 35 |
-
BOOK_STATES = 3000 # States the chapter averages the two terms of the bound over.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
# %% 02. The exact model on three tokens
|
| 39 |
-
#
|
| 40 |
-
# With a Gaussian source and the linear interpolant, the posterior over the data token is a
|
| 41 |
-
# softmax whose only state dependence is the coordinate of that token. The velocity follows from
|
| 42 |
-
# D_t(x) = x + (1-t) b(x,t), and the flow integrates to twelve digits with Runge-Kutta.
|
| 43 |
-
def exact_denoiser(x, t, prior):
|
| 44 |
-
"""pi_{t,t}(x), the posterior over the data token given the state. [B, K]."""
|
| 45 |
-
return torch.softmax(prior.log() + t * x / (1.0 - t) ** 2, dim=-1)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def exact_velocity(x, t, prior):
|
| 49 |
-
"""b(x,t) = (pi_{t,t}(x) - x)/(1 - t) for the linear interpolant. [B, K]."""
|
| 50 |
-
return (exact_denoiser(x, t, prior) - x) / (1.0 - t)
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def exact_flow(x, s, t, prior, n: int = 2000):
|
| 54 |
-
"""X_{s,t}(x) by fourth-order Runge-Kutta on the exact velocity."""
|
| 55 |
-
h = (t - s) / n
|
| 56 |
-
for k in range(n):
|
| 57 |
-
u = s + k * h
|
| 58 |
-
k1 = exact_velocity(x, u, prior)
|
| 59 |
-
k2 = exact_velocity(x + 0.5 * h * k1, u + 0.5 * h, prior)
|
| 60 |
-
k3 = exact_velocity(x + 0.5 * h * k2, u + 0.5 * h, prior)
|
| 61 |
-
k4 = exact_velocity(x + h * k3, u + h, prior)
|
| 62 |
-
x = x + (h / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
|
| 63 |
-
return x
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def exact_two_time(x, s, t, prior):
|
| 67 |
-
"""pi_{s,t}(x) read off the convex parameterization, [(1-s) X_{s,t}(x) - (1-t) x]/(t-s)."""
|
| 68 |
-
return ((1.0 - s) * exact_flow(x, s, t, prior) - (1.0 - t) * x) / (t - s)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
# %% 03. The coefficient, the gap and the drift
|
| 72 |
-
#
|
| 73 |
-
# gamma_{s,t} = (t-s)(1-t)/(1-s) vanishes on the diagonal and at t = 1 and never exceeds 1/4 on
|
| 74 |
-
# the square. The residual scaled by the remaining time splits as (1-t) r = a + b, with a the gap
|
| 75 |
-
# between the student's prediction and the prediction at the transported state.
|
| 76 |
-
def gamma_coefficient(s, t):
|
| 77 |
-
"""gamma_{s,t} = (t-s)(1-t)/(1-s)."""
|
| 78 |
-
return (t - s) * (1.0 - t) / (1.0 - s).clamp(min=1e-6)
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def end_time_derivative(x, s, t, prior, n: int = 2000):
|
| 82 |
-
"""d pi_{s,t}/dt from the integral representation, (1-s)/((t-s)(1-t)) times the bracket."""
|
| 83 |
-
transported = exact_flow(x, s, t, prior, n)
|
| 84 |
-
prediction = ((1.0 - s) * transported - (1.0 - t) * x) / (t - s)
|
| 85 |
-
at_output = exact_denoiser(transported, t, prior)
|
| 86 |
-
return (1.0 - s) / ((t - s) * (1.0 - t)) * (at_output - prediction), prediction, at_output
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
# %% 04. The worked cancellation of Section 6.28
|
| 90 |
-
#
|
| 91 |
-
# The derivative above comes from the closed form of Section 6.26, which already carries the
|
| 92 |
-
# cancellation inside it, so the central difference of pi_{s,t} in t below is the entry that checks
|
| 93 |
-
# the two pieces against each other without assuming the identity.
|
| 94 |
-
def worked_cancellation(s: float = 0.2, t: float = 0.6):
|
| 95 |
-
"""The three-token example, with every number integrated or differentiated here."""
|
| 96 |
-
prior = torch.tensor(BOOK_PRIOR, dtype=torch.float64)
|
| 97 |
-
source = torch.tensor(BOOK_SOURCE, dtype=torch.float64)
|
| 98 |
-
x = (1.0 - s) * source + s * torch.eye(3, dtype=torch.float64)[0] # Data token is the first.
|
| 99 |
-
derivative, prediction, at_output = end_time_derivative(x, s, t, prior)
|
| 100 |
-
gamma = (t - s) * (1.0 - t) / (1.0 - s)
|
| 101 |
-
gap = prediction - at_output # a_{s,t}, a difference of two simplex points.
|
| 102 |
-
drift = gamma * derivative # b_{s,t}, the scaled derivative of the prediction.
|
| 103 |
-
divergence = (at_output * (at_output / prediction).log()).sum() # KL(teacher || student).
|
| 104 |
-
difference = (exact_two_time(x, s, t + 1e-5, prior)
|
| 105 |
-
- exact_two_time(x, s, t - 1e-5, prior)) / 2e-5 # A central check.
|
| 106 |
-
return {
|
| 107 |
-
"state x": [round(v, 4) for v in x.tolist()],
|
| 108 |
-
"X_s,t(x)": [round(v, 4) for v in exact_flow(x, s, t, prior).tolist()],
|
| 109 |
-
"pi_s,t(x)": [round(v, 4) for v in prediction.tolist()],
|
| 110 |
-
"pi_t,t(X_s,t(x))": [round(v, 4) for v in at_output.tolist()],
|
| 111 |
-
"gamma_s,t": gamma,
|
| 112 |
-
"d pi / dt": [round(v, 4) for v in derivative.tolist()],
|
| 113 |
-
"central difference": [round(v, 4) for v in difference.tolist()],
|
| 114 |
-
"gap a": [round(v, 3) for v in gap.tolist()],
|
| 115 |
-
"drift b": [round(v, 3) for v in drift.tolist()],
|
| 116 |
-
"residual |a + b|^2": float(((gap + drift) ** 2).sum()),
|
| 117 |
-
"4 KL": 4.0 * float(divergence),
|
| 118 |
-
"2 |b|^2": 2.0 * float((drift ** 2).sum()),
|
| 119 |
-
"the bound": 4.0 * float(divergence) + 2.0 * float((drift ** 2).sum()),
|
| 120 |
-
"teacher entropy": -float((at_output * at_output.log()).sum()),
|
| 121 |
-
"cross-entropy": -float((at_output * prediction.log()).sum()),
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def averaged_bound(count: int = BOOK_STATES, s: float = 0.2, t: float = 0.6, seed: int = 0,
|
| 126 |
-
n: int = 400):
|
| 127 |
-
"""The same two terms averaged over states drawn from the interpolant at the same times."""
|
| 128 |
-
prior = torch.tensor(BOOK_PRIOR, dtype=torch.float64)
|
| 129 |
-
generator = torch.Generator().manual_seed(seed)
|
| 130 |
-
which = torch.multinomial(prior.expand(count, 3), 1, generator=generator).squeeze(1)
|
| 131 |
-
x1 = torch.eye(3, dtype=torch.float64)[which] # [N, 3] the lifted data token.
|
| 132 |
-
x0 = torch.randn(count, 3, generator=generator, dtype=torch.float64) # [N, 3] the source.
|
| 133 |
-
x = (1.0 - s) * x0 + s * x1 # [N, 3] the interpolant at the start time.
|
| 134 |
-
derivative, prediction, at_output = end_time_derivative(x, s, t, prior, n)
|
| 135 |
-
drift = ((t - s) * (1.0 - t) / (1.0 - s)) * derivative
|
| 136 |
-
divergence = (at_output * (at_output / prediction).log()).sum(-1).mean()
|
| 137 |
-
return {"averaged 4 KL": 4.0 * float(divergence),
|
| 138 |
-
"averaged 2 |b|^2": 2.0 * float((drift ** 2).sum(-1).mean()),
|
| 139 |
-
"averaged residual": float(((prediction - at_output + drift) ** 2).sum(-1).mean())}
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
# %% 05. The student, and the two losses it can be trained on
|
| 143 |
-
def predict(model, x, s, t, length, vocab):
|
| 144 |
-
"""pi_{s,t}(x) at every position, a softmax head over the vocabulary."""
|
| 145 |
-
return torch.softmax(model(x.flatten(1), s, t).view(-1, length, vocab), dim=-1)
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
def pieces(model, xs, s, t, length, vocab):
|
| 149 |
-
"""The gap a and the drift b of the split, with the teacher detached."""
|
| 150 |
-
f = lambda tt: predict(model, xs, s, tt, length, vocab)
|
| 151 |
-
prediction, derivative = torch.func.jvp(f, (t,), (torch.ones_like(t),)) # One forward mode pass.
|
| 152 |
-
keep, move = (1.0 - t) / (1.0 - s), (t - s) / (1.0 - s)
|
| 153 |
-
transported = keep[:, None, None] * xs + move[:, None, None] * prediction # X_{s,t}(x).
|
| 154 |
-
teacher = predict(model, transported.detach(), t, t, length, vocab).detach() # pi_{t,t}.
|
| 155 |
-
drift = gamma_coefficient(s, t)[:, None, None] * derivative
|
| 156 |
-
return prediction - teacher, drift, teacher, prediction
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
def make_loss(model, data, length, vocab, batch_size, device, bounded: bool):
|
| 160 |
-
eye = torch.eye(vocab, device=device)
|
| 161 |
-
|
| 162 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 163 |
-
index = torch.randint(0, len(data), (batch_size,), device=device)
|
| 164 |
-
x1, ids = eye[data[index]], data[index] # [B, L, K] and [B, L].
|
| 165 |
-
x0 = torch.randn_like(x1) # [B, L, K] the Gaussian source.
|
| 166 |
-
t_diag = torch.rand(batch_size, device=device)
|
| 167 |
-
xt = (1 - t_diag[:, None, None]) * x0 + t_diag[:, None, None] * x1
|
| 168 |
-
anchor = -(predict(model, xt, t_diag, t_diag, length, vocab).clamp_min(1e-9).log()
|
| 169 |
-
.gather(-1, ids[..., None])).mean() # Cross-entropy on the diagonal.
|
| 170 |
-
|
| 171 |
-
s = 0.9 * torch.rand(batch_size, device=device) # [B] start time, kept off t = 1.
|
| 172 |
-
t = s + (0.95 - s) * torch.rand(batch_size, device=device) # [B] end time below one.
|
| 173 |
-
xs = (1 - s[:, None, None]) * x0 + s[:, None, None] * x1
|
| 174 |
-
gap, drift, teacher, prediction = pieces(model, xs, s, t, length, vocab)
|
| 175 |
-
if bounded: # Four KL divergences plus two squared drifts, the bound of Section 6.28.
|
| 176 |
-
divergence = (teacher * (teacher.clamp_min(1e-9)
|
| 177 |
-
/ prediction.clamp_min(1e-9)).log()).sum(-1).mean()
|
| 178 |
-
return anchor + 4.0 * divergence + 2.0 * (drift ** 2).sum(-1).mean()
|
| 179 |
-
return anchor + ((gap + drift) ** 2).sum(-1).mean() # The residual itself.
|
| 180 |
-
|
| 181 |
-
return loss_fn
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
# %% 06. Sampling, and what a valid phrase is
|
| 185 |
-
@torch.no_grad()
|
| 186 |
-
def sample(model, count, length, vocab, steps, device):
|
| 187 |
-
"""Walk a uniform grid with the convex form and read the phrase off the last prediction."""
|
| 188 |
-
x = torch.randn(count, length, vocab, device=device)
|
| 189 |
-
grid = torch.linspace(0.0, 1.0, steps + 1)
|
| 190 |
-
for a, b in zip(grid[:-1], grid[1:]):
|
| 191 |
-
s, t = a.expand(count).to(device), b.expand(count).to(device)
|
| 192 |
-
prediction = predict(model, x, s, t, length, vocab)
|
| 193 |
-
keep, move = (1.0 - t) / (1.0 - s), (t - s) / (1.0 - s)
|
| 194 |
-
x = keep[:, None, None] * x + move[:, None, None] * prediction
|
| 195 |
-
return x.argmax(-1) # [N, L].
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
def valid_fraction(drawn, group: int = 4):
|
| 199 |
-
"""A phrase is in the language when position i holds a token from the i-th quarter."""
|
| 200 |
-
slots = torch.arange(drawn.shape[1], device=drawn.device) # [L].
|
| 201 |
-
return float(((drawn // group) == slots[None, :]).all(dim=1).to(torch.get_default_dtype()).mean())
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
# %% 07. The report
|
| 205 |
-
def main() -> None:
|
| 206 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 207 |
-
parser.set_defaults(steps=1000, batch_size=128, samples=256, sample_steps=4)
|
| 208 |
-
args = parser.parse_args()
|
| 209 |
-
set_seed(args.seed)
|
| 210 |
-
device = pick_device(args.device)
|
| 211 |
-
tokens, vocabulary = phrases(num_phrases=1024, seed=args.seed)
|
| 212 |
-
data, length, vocab = tokens.to(device), tokens.shape[1], len(vocabulary)
|
| 213 |
-
|
| 214 |
-
report = worked_cancellation()
|
| 215 |
-
report.update(averaged_bound(seed=args.seed)) # The chapter's three thousand states.
|
| 216 |
-
if not args.quiet:
|
| 217 |
-
print(f"gap {report['gap a']} and drift {report['drift b']} cancel to "
|
| 218 |
-
f"{report['residual |a + b|^2']:.2e}, bound {report['the bound']:.3f}")
|
| 219 |
-
|
| 220 |
-
samples: list[str] = []
|
| 221 |
-
for name, bounded in (("the residual", False), ("the bound", True)):
|
| 222 |
-
set_seed(args.seed) # Both students start from the same weights and see the same batches.
|
| 223 |
-
model = TwoTimeMLP(dim=length * vocab, width=args.width).to(device)
|
| 224 |
-
if not args.quiet:
|
| 225 |
-
print(f"\ntraining on {name} parameters={count_parameters(model):,}")
|
| 226 |
-
losses = train(model, make_loss(model, data, length, vocab, args.batch_size, device,
|
| 227 |
-
bounded), steps=args.steps, lr=args.lr, quiet=args.quiet)
|
| 228 |
-
for steps in (1, args.sample_steps):
|
| 229 |
-
torch.manual_seed(args.seed + 1)
|
| 230 |
-
drawn = sample(model, args.samples, length, vocab, steps, device)
|
| 231 |
-
report[f"{name}, {steps} step(s): valid"] = valid_fraction(drawn)
|
| 232 |
-
report[f"{name}, {steps} step(s): distinct"] = unique_fraction(drawn)
|
| 233 |
-
report[f"{name}, {steps} step(s): entropy"] = token_entropy(drawn, vocab)
|
| 234 |
-
report[f"{name}: final loss"] = sum(losses[-20:]) / len(losses[-20:])
|
| 235 |
-
drawn = sample(model, 4, length, vocab, args.sample_steps, device)
|
| 236 |
-
samples.append(f"{name:<14s} " + " | ".join(
|
| 237 |
-
" ".join(vocabulary[int(i)] for i in row) for row in drawn[:2]))
|
| 238 |
-
|
| 239 |
-
print_report("Categorical Flow Maps, Chapter 6", report, samples)
|
| 240 |
-
save_run(args.out, config=vars(args), report=report, samples=samples)
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
if __name__ == "__main__":
|
| 244 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_6/consistency.py
DELETED
|
@@ -1,231 +0,0 @@
|
|
| 1 |
-
"""Consistency models and consistency trajectory models, one time fixed and then both free.
|
| 2 |
-
|
| 3 |
-
A consistency model fixes the end time at the data end and learns the top edge of the triangle of
|
| 4 |
-
start and end times, with the identity axiom imposed at the corner by the boundary coefficients
|
| 5 |
-
c_skip and c_out. A consistency trajectory model puts the second time back and learns the whole
|
| 6 |
-
triangle, writing the map as a weighted average of the input and a head that predicts a
|
| 7 |
-
destination. Both are distilled here from a teacher velocity available in closed form.
|
| 8 |
-
|
| 9 |
-
Chapter 6, Section 6.20 of the notes. Trained on the four Gaussian blobs of
|
| 10 |
-
``cis6270.data.gaussian_mixture_2d``. Prints the boundary parameterization on dx/dt = x, where the
|
| 11 |
-
model returns e^{0.5}, e^{0.1} and 1 at three start times, the teacher's error floor of
|
| 12 |
-
8.189e-05, and the destination head of a consistency trajectory model at two pairs of times.
|
| 13 |
-
|
| 14 |
-
python lecture_6/consistency.py # two models, 1500 steps each
|
| 15 |
-
python lecture_6/consistency.py --steps 20 # a few seconds, for a first look
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
import math
|
| 20 |
-
|
| 21 |
-
import torch
|
| 22 |
-
|
| 23 |
-
from cis6270.data import gaussian_mixture_2d
|
| 24 |
-
from cis6270.metrics import mean_log_density, mode_report
|
| 25 |
-
from cis6270.nets import TwoTimeMLP, count_parameters
|
| 26 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed, train
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
torch.set_num_threads(1) # These networks are small, and splitting one matrix product costs more
|
| 30 |
-
# than it saves. ``tests/conftest.py`` sets the same thing for the test session, so the line
|
| 31 |
-
# here is the one that acts when a reader runs this script directly.
|
| 32 |
-
|
| 33 |
-
TEACHER_STEP = 0.02 # h, the length of the teacher's solver step inside the distillation loss.
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
# %% 02. The teacher velocity, in closed form on the empirical data
|
| 37 |
-
#
|
| 38 |
-
# The posterior over the data point given the interpolant is a softmax over the training set, so
|
| 39 |
-
# the exact marginal velocity of the four blobs needs no training. A trained flow matching model
|
| 40 |
-
# would serve in its place, with its own error on top of the solver's.
|
| 41 |
-
def teacher_denoiser(x, t, data):
|
| 42 |
-
"""D_t(x) = E[X_1 | X_t = x], the posterior mean over the training points. [B, 2]."""
|
| 43 |
-
spread = (1.0 - t).clamp(min=1e-4)[:, None] # [B, 1] the width of the conditional.
|
| 44 |
-
squared = ((x[:, None, :] - t[:, None, None] * data[None]) ** 2).sum(-1) # [B, N].
|
| 45 |
-
weights = torch.softmax(-squared / (2.0 * spread ** 2), dim=1) # [B, N] posterior over data.
|
| 46 |
-
return weights @ data # [B, 2] the posterior mean.
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def teacher_velocity(x, t, data):
|
| 50 |
-
"""b(x,t) = (D_t(x) - x)/(1 - t) for the linear interpolant. [B, 2]."""
|
| 51 |
-
return (teacher_denoiser(x, t, data) - x) / (1.0 - t).clamp(min=1e-4)[:, None]
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
def solver_step(x, t, h, data):
|
| 55 |
-
"""One Euler step of the teacher, which the chapter writes as Phi_h."""
|
| 56 |
-
return x + h * teacher_velocity(x, t, data) # [B, 2] the state at time t + h.
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
# %% 03. The boundary parameterization, checked on the scalar flow
|
| 60 |
-
#
|
| 61 |
-
# f_theta(x, s) = c_skip(s) x + c_out(s) F_theta(x, s) with c_skip = 1 and c_out = 1 - s. At the
|
| 62 |
-
# data end the output coefficient is zero and the model returns its input, so no training signal
|
| 63 |
-
# is spent on a condition we already know.
|
| 64 |
-
def scalar_consistency(x_value: float = 1.0):
|
| 65 |
-
"""The velocity head, the output coefficient and the model at three start times."""
|
| 66 |
-
out = {}
|
| 67 |
-
for s in (0.5, 0.9, 1.0):
|
| 68 |
-
remaining = 1.0 - s # c_out(s), which vanishes at the data end.
|
| 69 |
-
head = x_value * math.expm1(remaining) / remaining if remaining > 0 else x_value
|
| 70 |
-
out[f"c_out({s})"] = remaining
|
| 71 |
-
out[f"v_{s},1(1)"] = head
|
| 72 |
-
out[f"f({s})"] = x_value + remaining * head # Equals e^{1-s} on this flow.
|
| 73 |
-
return out
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def teacher_floor(x_value: float = 1.0, s: float = 0.5, h: float = 0.01):
|
| 77 |
-
"""The distillation residual at the exact map, which is the teacher's own Euler error."""
|
| 78 |
-
exact = lambda x, t: x * math.exp(1.0 - t) # f(x, t) = x e^{1-t} on dx/dt = x.
|
| 79 |
-
moved = x_value + h * x_value # One teacher Euler step, which undershoots e^{h}.
|
| 80 |
-
residual = exact(x_value, s) - exact(moved, s + h)
|
| 81 |
-
local = math.exp(h) - 1.0 - h # The local error of that step.
|
| 82 |
-
return {"f(1, 0.5)": exact(x_value, s), "f(1.01, 0.51)": exact(moved, s + h),
|
| 83 |
-
"distillation residual": residual, "local Euler error": local,
|
| 84 |
-
"carried to the data end": local * math.exp(1.0 - s - h)}
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
# %% 04. Consistency trajectory models, with both times free
|
| 88 |
-
#
|
| 89 |
-
# X_theta(x, s, t) = a_{s,t} x + (1 - a_{s,t}) g_theta(x, s, t) with a_{s,t} = (1-t)/(1-s), which
|
| 90 |
-
# equals one on the diagonal, so the map returns its input there for any head whatever.
|
| 91 |
-
def ctm_weight(s, t):
|
| 92 |
-
"""a_{s,t} = (1-t)/(1-s), the share of the input kept by the step."""
|
| 93 |
-
return (1.0 - t) / (1.0 - s).clamp(min=1e-4)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def ctm_map(model, x, s, t):
|
| 97 |
-
"""The weighted average of the input and the destination head."""
|
| 98 |
-
a = ctm_weight(s, t)[:, None] # [B, 1].
|
| 99 |
-
return a * x + (1.0 - a) * model(x, s, t) # [B, 2].
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def scalar_ctm(x_value: float = 1.0):
|
| 103 |
-
"""The head solved from the exact map at two pairs of times, and the two-route sampler."""
|
| 104 |
-
out = {}
|
| 105 |
-
for s, t in ((0.0, 0.5), (0.0, 1.0)):
|
| 106 |
-
a = (1.0 - t) / (1.0 - s)
|
| 107 |
-
reached = x_value * math.exp(t - s) # The exact map of dx/dt = x.
|
| 108 |
-
out[f"a_{s},{t}"] = a
|
| 109 |
-
out[f"g_{s},{t}"] = (reached - a * x_value) / (1.0 - a) # Solve the weighted average.
|
| 110 |
-
short = x_value * math.exp(0.5) # X_{0,0.5}(1), the direct short step.
|
| 111 |
-
long_then_back = (x_value * math.exp(1.0)) * math.exp(-0.5) # X_{1,0.5}(X_{0,1}(1)).
|
| 112 |
-
out["short step"], out["long jump and return"] = short, long_then_back
|
| 113 |
-
out["two routes apart"] = abs(short - long_then_back)
|
| 114 |
-
return out
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
# %% 05. The two losses
|
| 118 |
-
#
|
| 119 |
-
# Both compare the model at one time against the model at a state the teacher has advanced by h,
|
| 120 |
-
# with the target detached, which is the frozen copy of the weights the chapter writes as theta
|
| 121 |
-
# minus. The consistency model holds the end time at one; the trajectory model draws it.
|
| 122 |
-
def make_consistency_loss(model, data, batch_size, device):
|
| 123 |
-
ones = torch.ones(batch_size, device=device)
|
| 124 |
-
|
| 125 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 126 |
-
index = torch.randint(0, len(data), (batch_size,), device=device)
|
| 127 |
-
x1, x0 = data[index], torch.randn(batch_size, 2, device=device)
|
| 128 |
-
s = (1.0 - TEACHER_STEP) * torch.rand(batch_size, device=device) # [B] with s + h <= 1.
|
| 129 |
-
xs = (1 - s[:, None]) * x0 + s[:, None] * x1 # [B, 2] the interpolant at the start time.
|
| 130 |
-
with torch.no_grad(): # The second leg is the teacher's, and no gradient reaches it.
|
| 131 |
-
advanced = solver_step(xs, s, TEACHER_STEP, data)
|
| 132 |
-
target = model.flow_map(advanced, s + TEACHER_STEP, ones) # f_{theta-}(Phi_h(x_s), s+h).
|
| 133 |
-
return ((model.flow_map(xs, s, ones) - target) ** 2).mean()
|
| 134 |
-
|
| 135 |
-
return loss_fn
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
def make_trajectory_loss(model, data, batch_size, device):
|
| 139 |
-
|
| 140 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 141 |
-
index = torch.randint(0, len(data), (batch_size,), device=device)
|
| 142 |
-
x1, x0 = data[index], torch.randn(batch_size, 2, device=device)
|
| 143 |
-
s = (1.0 - TEACHER_STEP) * torch.rand(batch_size, device=device) # [B] start time.
|
| 144 |
-
t = s + TEACHER_STEP + (1.0 - s - TEACHER_STEP) * torch.rand(batch_size, device=device)
|
| 145 |
-
xs = (1 - s[:, None]) * x0 + s[:, None] * x1 # [B, 2].
|
| 146 |
-
with torch.no_grad():
|
| 147 |
-
advanced = solver_step(xs, s, TEACHER_STEP, data)
|
| 148 |
-
target = ctm_map(model, advanced, s + TEACHER_STEP, t) # The same end time, one h later.
|
| 149 |
-
return ((ctm_map(model, xs, s, t) - target) ** 2).mean()
|
| 150 |
-
|
| 151 |
-
return loss_fn
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
# %% 06. Sampling
|
| 155 |
-
@torch.no_grad()
|
| 156 |
-
def consistency_sample(model, x0):
|
| 157 |
-
"""One call of the edge map, from the source straight to the data end."""
|
| 158 |
-
zeros = torch.zeros(len(x0), device=x0.device)
|
| 159 |
-
return model.flow_map(x0, zeros, torch.ones_like(zeros)) # [N, 2].
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
@torch.no_grad()
|
| 163 |
-
def trajectory_sample(model, x0, steps: int):
|
| 164 |
-
"""Cross the triangle in equal pieces, one evaluation of the weighted average per piece."""
|
| 165 |
-
x = x0
|
| 166 |
-
grid = torch.linspace(0.0, 1.0, steps + 1, device=x0.device) # [steps+1] the time grid.
|
| 167 |
-
for s, t in zip(grid[:-1], grid[1:]):
|
| 168 |
-
x = ctm_map(model, x, s.expand(len(x)), t.expand(len(x)))
|
| 169 |
-
return x # [N, 2].
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
# %% 07. The report
|
| 173 |
-
def main() -> None:
|
| 174 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 175 |
-
parser.set_defaults(steps=1500, batch_size=128, samples=512)
|
| 176 |
-
args = parser.parse_args()
|
| 177 |
-
set_seed(args.seed)
|
| 178 |
-
device = pick_device(args.device)
|
| 179 |
-
data = gaussian_mixture_2d(num_points=1024, seed=args.seed).to(device) # [1024, 2].
|
| 180 |
-
|
| 181 |
-
report = scalar_consistency()
|
| 182 |
-
report.update(teacher_floor())
|
| 183 |
-
report.update(scalar_ctm())
|
| 184 |
-
if not args.quiet:
|
| 185 |
-
print(f"boundary parameterization: f(0.5) = {report['f(0.5)']:.6f}, "
|
| 186 |
-
f"f(0.9) = {report['f(0.9)']:.6f}, f(1.0) = {report['f(1.0)']:.6f}")
|
| 187 |
-
print(f"teacher floor {report['distillation residual']:.6e} against the carried local "
|
| 188 |
-
f"error {report['carried to the data end']:.6e}")
|
| 189 |
-
|
| 190 |
-
samples: list[str] = []
|
| 191 |
-
set_seed(args.seed)
|
| 192 |
-
consistency = TwoTimeMLP(dim=2, width=args.width).to(device)
|
| 193 |
-
if not args.quiet:
|
| 194 |
-
print(f"\ntraining the consistency model parameters={count_parameters(consistency):,}")
|
| 195 |
-
edge_losses = train(consistency,
|
| 196 |
-
make_consistency_loss(consistency, data, args.batch_size, device),
|
| 197 |
-
steps=args.steps, lr=args.lr, quiet=args.quiet)
|
| 198 |
-
report["consistency final loss"] = sum(edge_losses[-20:]) / len(edge_losses[-20:])
|
| 199 |
-
torch.manual_seed(args.seed + 1)
|
| 200 |
-
x0 = torch.randn(args.samples, 2, device=device) # [N, 2] the shared source points.
|
| 201 |
-
points = consistency_sample(consistency, x0)
|
| 202 |
-
stats = mode_report(points)
|
| 203 |
-
report["consistency one-step nearest"] = stats["nearest_distance"]
|
| 204 |
-
report["consistency one-step log density"] = mean_log_density(points.cpu())
|
| 205 |
-
samples.append(f"consistency, 1 step: nearest {stats['nearest_distance']:.3f} "
|
| 206 |
-
f"modes {stats['modes_found']} entropy {stats['mode_entropy']:.3f}")
|
| 207 |
-
|
| 208 |
-
set_seed(args.seed)
|
| 209 |
-
trajectory = TwoTimeMLP(dim=2, width=args.width).to(device)
|
| 210 |
-
if not args.quiet:
|
| 211 |
-
print(f"\ntraining the trajectory model parameters={count_parameters(trajectory):,}")
|
| 212 |
-
losses = train(trajectory, make_trajectory_loss(trajectory, data, args.batch_size, device),
|
| 213 |
-
steps=args.steps, lr=args.lr, quiet=args.quiet)
|
| 214 |
-
for steps in (1, 2, 4):
|
| 215 |
-
points = trajectory_sample(trajectory, x0, steps)
|
| 216 |
-
stats = mode_report(points)
|
| 217 |
-
report[f"trajectory {steps}-step nearest"] = stats["nearest_distance"]
|
| 218 |
-
samples.append(f"trajectory, {steps} step(s): nearest {stats['nearest_distance']:.3f} "
|
| 219 |
-
f"modes {stats['modes_found']} entropy {stats['mode_entropy']:.3f}")
|
| 220 |
-
report["trajectory one step against four"] = float(
|
| 221 |
-
(trajectory_sample(trajectory, x0, 1) - trajectory_sample(trajectory, x0, 4)).norm(dim=1)
|
| 222 |
-
.mean())
|
| 223 |
-
report["trajectory final loss"] = sum(losses[-20:]) / len(losses[-20:])
|
| 224 |
-
|
| 225 |
-
print_report("Consistency and consistency trajectory models, Chapter 6", report, samples)
|
| 226 |
-
save_run(args.out, model=trajectory, config=vars(args), report=report, losses=losses,
|
| 227 |
-
samples=samples)
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
if __name__ == "__main__":
|
| 231 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_6/discrete_map.py
DELETED
|
@@ -1,218 +0,0 @@
|
|
| 1 |
-
"""Discrete Flow Maps, the derivative identities repaired in logit space.
|
| 2 |
-
|
| 3 |
-
A curve that stays on the simplex has coordinates summing to one, so its velocity has coordinates
|
| 4 |
-
summing to zero, and a target built from that velocity has a negative entry as soon as one
|
| 5 |
-
coordinate of the prediction is smaller than the derivative pulling it down. Written in logits the
|
| 6 |
-
same derivative becomes a positive multiplicative factor, dividing by it keeps every coordinate
|
| 7 |
-
positive, and a softmax normalizes what is left, so the Kullback-Leibler divergence against the
|
| 8 |
-
repaired target is defined at every state.
|
| 9 |
-
|
| 10 |
-
Chapter 6, Sections 6.26 and 6.29 of the notes. Trained on the synthetic DNA of
|
| 11 |
-
``cis6270.data.synthetic_dna``. Prints the derivative target of Section 6.26, three of whose four
|
| 12 |
-
entries are negative, the three-token repair, where the additive target (-0.284, 0.828, 0.456)
|
| 13 |
-
becomes the distribution (0.0194, 0.6259, 0.3547), and samples from the trained map.
|
| 14 |
-
|
| 15 |
-
python lecture_6/discrete_map.py # 1500 steps, about half a minute
|
| 16 |
-
python lecture_6/discrete_map.py --steps 20 # a few seconds, for a first look
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
# %% 01. Imports and configuration
|
| 20 |
-
import torch
|
| 21 |
-
|
| 22 |
-
from cis6270.data import DNA_SIZE, decode_dna, synthetic_dna
|
| 23 |
-
from cis6270.metrics import gc_fraction, motif_fraction, token_entropy, unique_fraction
|
| 24 |
-
from cis6270.nets import TwoTimeMLP, count_parameters
|
| 25 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed, train
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
torch.set_num_threads(1) # These networks are small, and splitting one matrix product costs more
|
| 29 |
-
# than it saves. ``tests/conftest.py`` sets the same thing for the test session, so the line
|
| 30 |
-
# here is the one that acts when a reader runs this script directly.
|
| 31 |
-
|
| 32 |
-
FLOOR = 1e-3 # The factor is clipped away from zero before the logarithm, as the chapter notes.
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
# %% 02. Why the derivative target leaves the simplex
|
| 36 |
-
#
|
| 37 |
-
# d delta_{s,t}/dt = (1-s)/((t-s)(1-t)) [D_t(X_{s,t}(x)) - delta_{s,t}(x)]. The bracket is a
|
| 38 |
-
# difference of two simplex points, so its coordinates sum to zero, and the prefactor only scales
|
| 39 |
-
# it up. Nothing about the alphabet or the prior changes the sign pattern.
|
| 40 |
-
def derivative_target(prediction, at_output, s: float, t: float):
|
| 41 |
-
"""The end-time derivative of the two-time denoiser, a direction and not a distribution."""
|
| 42 |
-
prefactor = (1.0 - s) / ((t - s) * (1.0 - t)) # 6.6667 at s = 0.2, t = 0.8.
|
| 43 |
-
return prefactor * (at_output - prediction), prefactor
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def worked_derivative():
|
| 47 |
-
"""The chapter's DNA example at s = 0.2, t = 0.8, whose target has three negative entries."""
|
| 48 |
-
prediction = torch.tensor([0.10, 0.20, 0.60, 0.10]) # delta_{0.2,0.8} over (A, C, G, T).
|
| 49 |
-
at_output = torch.tensor([0.05, 0.10, 0.80, 0.05]) # D_{0.8} at the transported state.
|
| 50 |
-
target, prefactor = derivative_target(prediction, at_output, 0.2, 0.8)
|
| 51 |
-
return {"derivative prefactor": prefactor,
|
| 52 |
-
"bracket": [round(v, 4) for v in (at_output - prediction).tolist()],
|
| 53 |
-
"derivative target": [round(v, 4) for v in target.tolist()],
|
| 54 |
-
"target sum": float(target.sum()),
|
| 55 |
-
"negative entries": int((target < 0).sum())}
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
# %% 03. Differentiating a softmax
|
| 59 |
-
#
|
| 60 |
-
# The Jacobian of the softmax is diag(psi) - psi psi^T, so contracting it with the logit
|
| 61 |
-
# derivative gives psi times the centered derivative. The centring makes the coordinates of
|
| 62 |
-
# d psi/dt sum to zero and makes the identity insensitive to the shift invariance of the softmax.
|
| 63 |
-
def centered(prediction, logit_derivative):
|
| 64 |
-
"""c_{s,t} = d z/dt - <psi, d z/dt> 1, the quantity the whole identity is built on."""
|
| 65 |
-
mean = (prediction * logit_derivative).sum(-1, keepdim=True) # <psi, dz/dt>.
|
| 66 |
-
return logit_derivative - mean # [..., K] with <psi, c> = 0.
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
# %% 04. The identity as a multiplicative relation, and the repaired teacher
|
| 70 |
-
#
|
| 71 |
-
# psi_{t,t}(X_{s,t}(x)) = psi_{s,t}(x) * (1 + gamma c_{s,t}(x)) coordinatewise. Solving for the
|
| 72 |
-
# student divides by a strictly positive factor, and in logits the division is a subtraction of
|
| 73 |
-
# its logarithm, so the target is a softmax of an expression the network already produces.
|
| 74 |
-
def gamma_lagrangian(s, t):
|
| 75 |
-
"""gamma_{s,t} = (t-s)(1-t)/(1-s), which never exceeds 1/4 on the square."""
|
| 76 |
-
return (t - s) * (1.0 - t) / (1.0 - s).clamp(min=1e-6)
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def gamma_eulerian(s, t):
|
| 80 |
-
"""gamma^E_{s,t} = (1-s)(t-s)/(1-t), larger than the Lagrangian one and growing toward t = 1."""
|
| 81 |
-
return (1.0 - s) * (t - s) / (1.0 - t).clamp(min=1e-6)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def repaired_teacher(teacher_logits, factor):
|
| 85 |
-
"""Softmax(z_{t,t}(X_{s,t}(x)) - log(1 + gamma c)), a distribution by construction."""
|
| 86 |
-
return torch.softmax(teacher_logits - factor.clamp(min=FLOOR).log(), dim=-1)
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
# %% 05. The repair worked on three tokens
|
| 90 |
-
def worked_repair(s: float = 0.2, t: float = 0.6):
|
| 91 |
-
"""The chapter's three-token repair, with the additive target beside the multiplicative one."""
|
| 92 |
-
prediction = torch.tensor([0.2, 0.5, 0.3]) # psi_{s,t}(x).
|
| 93 |
-
logit_derivative = torch.tensor([9.60, -2.28, -2.60]) # d z_{s,t}/dt.
|
| 94 |
-
gamma = (t - s) * (1.0 - t) / (1.0 - s)
|
| 95 |
-
c = centered(prediction, logit_derivative) # Already centered on this example.
|
| 96 |
-
derivative = prediction * c # d psi/dt, whose coordinates sum to zero.
|
| 97 |
-
teacher = torch.tensor([0.10, 0.60, 0.30]) # psi_{t,t}(X_{s,t}(x)) during training.
|
| 98 |
-
additive = teacher - gamma * derivative # Read straight off the identity.
|
| 99 |
-
factor = 1.0 + gamma * c # The multiplicative factor, positive in every coordinate.
|
| 100 |
-
quotient = teacher / factor # The teacher divided coordinatewise.
|
| 101 |
-
return {
|
| 102 |
-
"weighted average of dz/dt": float((prediction * logit_derivative).sum()),
|
| 103 |
-
"centered derivative": [round(v, 4) for v in c.tolist()],
|
| 104 |
-
"d psi/dt": [round(v, 4) for v in derivative.tolist()],
|
| 105 |
-
"d psi/dt sum": float(derivative.sum()),
|
| 106 |
-
"additive target": [round(v, 4) for v in additive.tolist()],
|
| 107 |
-
"additive target sum": float(additive.sum()),
|
| 108 |
-
"additive negative entries": int((additive < 0).sum()),
|
| 109 |
-
"multiplicative factor": [round(v, 4) for v in factor.tolist()],
|
| 110 |
-
"quotient": [round(v, 4) for v in quotient.tolist()],
|
| 111 |
-
"quotient mass": float(quotient.sum()),
|
| 112 |
-
"repaired target": [round(v, 4) for v in (quotient / quotient.sum()).tolist()],
|
| 113 |
-
"log of the factor": [round(v, 4) for v in factor.log().tolist()],
|
| 114 |
-
"gamma_s,t": gamma,
|
| 115 |
-
"gamma^E_s,t": (1.0 - s) * (t - s) / (1.0 - t),
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
# %% 06. The student, and the loss built on the repaired target
|
| 120 |
-
def logits_of(model, x, s, t, length, vocab):
|
| 121 |
-
"""z_{s,t}(x), one vector of length K per position."""
|
| 122 |
-
return model(x.flatten(1), s, t).view(-1, length, vocab) # [B, L, K].
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def make_loss(model, data, length, vocab, batch_size, device, counter):
|
| 126 |
-
eye = torch.eye(vocab, device=device)
|
| 127 |
-
|
| 128 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 129 |
-
index = torch.randint(0, len(data), (batch_size,), device=device)
|
| 130 |
-
ids = data[index] # [B, L] the data tokens.
|
| 131 |
-
x1, x0 = eye[ids], torch.randn(batch_size, length, vocab, device=device)
|
| 132 |
-
t_diag = torch.rand(batch_size, device=device)
|
| 133 |
-
xt = (1 - t_diag[:, None, None]) * x0 + t_diag[:, None, None] * x1
|
| 134 |
-
diagonal_logits = logits_of(model, xt, t_diag, t_diag, length, vocab)
|
| 135 |
-
anchor = torch.nn.functional.cross_entropy( # KL to the one-hot token, the diagonal anchor.
|
| 136 |
-
diagonal_logits.reshape(-1, vocab), ids.reshape(-1))
|
| 137 |
-
|
| 138 |
-
s = 0.9 * torch.rand(batch_size, device=device) # [B] start time.
|
| 139 |
-
t = s + (0.95 - s) * torch.rand(batch_size, device=device) # [B] end time below one.
|
| 140 |
-
xs = (1 - s[:, None, None]) * x0 + s[:, None, None] * x1 # [B, L, K].
|
| 141 |
-
f = lambda tt: logits_of(model, xs, s, tt, length, vocab)
|
| 142 |
-
z, dz = torch.func.jvp(f, (t,), (torch.ones_like(t),)) # One forward-mode pass in t.
|
| 143 |
-
prediction = torch.softmax(z, dim=-1) # psi_{s,t}(x).
|
| 144 |
-
gamma = gamma_lagrangian(s, t)[:, None, None]
|
| 145 |
-
factor = 1.0 + gamma * centered(prediction, dz) # The multiplicative factor.
|
| 146 |
-
keep, move = (1.0 - t) / (1.0 - s), (t - s) / (1.0 - s)
|
| 147 |
-
transported = keep[:, None, None] * xs + move[:, None, None] * prediction # X_{s,t}(x).
|
| 148 |
-
with torch.no_grad(): # The teacher is the same network read on the diagonal, detached.
|
| 149 |
-
teacher_logits = logits_of(model, transported.detach(), t, t, length, vocab)
|
| 150 |
-
target = repaired_teacher(teacher_logits, factor.detach())
|
| 151 |
-
additive = torch.softmax(teacher_logits, -1) - gamma * prediction * centered(
|
| 152 |
-
prediction, dz) # The unrepaired target, measured and not trained on.
|
| 153 |
-
counter["negative"] += float((additive < 0).to(torch.get_default_dtype()).mean())
|
| 154 |
-
counter["batches"] += 1.0
|
| 155 |
-
student = torch.log_softmax(z, dim=-1) # The student in log space.
|
| 156 |
-
return anchor + (target * ((target.clamp_min(1e-9)).log() - student)).sum(-1).mean()
|
| 157 |
-
|
| 158 |
-
return loss_fn
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
# %% 07. Sampling
|
| 162 |
-
@torch.no_grad()
|
| 163 |
-
def sample(model, count, length, vocab, steps, device):
|
| 164 |
-
"""Walk a uniform grid with the convex form and read the alphabet off the last prediction."""
|
| 165 |
-
x = torch.randn(count, length, vocab, device=device)
|
| 166 |
-
grid = torch.linspace(0.0, 1.0, steps + 1)
|
| 167 |
-
for a, b in zip(grid[:-1], grid[1:]):
|
| 168 |
-
s, t = a.expand(count).to(device), b.expand(count).to(device)
|
| 169 |
-
prediction = torch.softmax(logits_of(model, x, s, t, length, vocab), dim=-1)
|
| 170 |
-
keep, move = (1.0 - t) / (1.0 - s), (t - s) / (1.0 - s)
|
| 171 |
-
x = keep[:, None, None] * x + move[:, None, None] * prediction
|
| 172 |
-
return x.argmax(-1) # [N, L].
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
# %% 08. The report
|
| 176 |
-
def main() -> None:
|
| 177 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 178 |
-
parser.set_defaults(steps=1500, batch_size=128, samples=64, length=16, sample_steps=4)
|
| 179 |
-
args = parser.parse_args()
|
| 180 |
-
set_seed(args.seed)
|
| 181 |
-
device = pick_device(args.device)
|
| 182 |
-
tokens, _ = synthetic_dna(num_sequences=1024, length=args.length, seed=args.seed)
|
| 183 |
-
data = tokens.to(device) # [1024, L].
|
| 184 |
-
|
| 185 |
-
report = worked_derivative()
|
| 186 |
-
report.update(worked_repair())
|
| 187 |
-
if not args.quiet:
|
| 188 |
-
print(f"derivative target {report['derivative target']} has "
|
| 189 |
-
f"{report['negative entries']} negative entries and sums to "
|
| 190 |
-
f"{report['target sum']:.4f}")
|
| 191 |
-
print(f"additive {report['additive target']} repairs to {report['repaired target']}")
|
| 192 |
-
|
| 193 |
-
counter = {"negative": 0.0, "batches": 0.0}
|
| 194 |
-
model = TwoTimeMLP(dim=args.length * DNA_SIZE, width=args.width).to(device)
|
| 195 |
-
if not args.quiet:
|
| 196 |
-
print(f"\ntraining with the repaired target parameters={count_parameters(model):,}")
|
| 197 |
-
losses = train(model, make_loss(model, data, args.length, DNA_SIZE, args.batch_size, device,
|
| 198 |
-
counter), steps=args.steps, lr=args.lr, quiet=args.quiet)
|
| 199 |
-
report["additive target negative share"] = counter["negative"] / max(counter["batches"], 1.0)
|
| 200 |
-
|
| 201 |
-
samples: list[str] = []
|
| 202 |
-
for steps in (1, args.sample_steps):
|
| 203 |
-
torch.manual_seed(args.seed + 1) # The same source noise at every step count.
|
| 204 |
-
drawn = sample(model, args.samples, args.length, DNA_SIZE, steps, device)
|
| 205 |
-
report[f"{steps}-step GC fraction"] = gc_fraction(drawn)
|
| 206 |
-
report[f"{steps}-step motif fraction"] = motif_fraction(drawn)
|
| 207 |
-
report[f"{steps}-step token entropy"] = token_entropy(drawn, DNA_SIZE)
|
| 208 |
-
report[f"{steps}-step unique fraction"] = unique_fraction(drawn)
|
| 209 |
-
samples.append(f"{steps} step(s): {decode_dna(drawn[:1])[0]} GC {gc_fraction(drawn):.3f}")
|
| 210 |
-
report["training GC fraction"] = gc_fraction(data)
|
| 211 |
-
report["final loss"] = sum(losses[-20:]) / len(losses[-20:])
|
| 212 |
-
|
| 213 |
-
print_report("Discrete Flow Maps, Chapter 6", report, samples)
|
| 214 |
-
save_run(args.out, model=model, config=vars(args), report=report, losses=losses, samples=samples)
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
if __name__ == "__main__":
|
| 218 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_6/expanding.py
DELETED
|
@@ -1,217 +0,0 @@
|
|
| 1 |
-
"""Expanding flow maps, where the state gains coordinates as time runs.
|
| 2 |
-
|
| 3 |
-
The Jacobian of a map between states of different length is rectangular, so it has no determinant
|
| 4 |
-
and no inverse, and Liouville's formula, the exact likelihood and the inverse map all stop
|
| 5 |
-
applying. Splitting the move into an expansion that fills new coordinates with source noise and a
|
| 6 |
-
transport that acts at the longer length restores a square Jacobian. Each coordinate then carries
|
| 7 |
-
its own clock, rescaled to the interval it occupies, and the composition identity is still
|
| 8 |
-
available, because a finite difference between two maps needs no derivative in time.
|
| 9 |
-
|
| 10 |
-
Chapter 6, Section 6.32 of the notes. Trained on the synthetic DNA of
|
| 11 |
-
``cis6270.data.synthetic_dna``, grown from half its length to all of it. Prints the local clocks
|
| 12 |
-
of the chapter's figure, which read 0.800, 0.692 and 0.333 at a global time of 0.8, the
|
| 13 |
-
composition weight 0.125 at (0.3, 0.6, 0.9), and samples from the trained map.
|
| 14 |
-
|
| 15 |
-
python lecture_6/expanding.py # 1500 steps, about half a minute
|
| 16 |
-
python lecture_6/expanding.py --steps 20 # a few seconds, for a first look
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
# %% 01. Imports and configuration
|
| 20 |
-
import math
|
| 21 |
-
|
| 22 |
-
import torch
|
| 23 |
-
|
| 24 |
-
from cis6270.data import DNA_SIZE, decode_dna, synthetic_dna
|
| 25 |
-
from cis6270.metrics import gc_fraction, motif_fraction, token_entropy, unique_fraction
|
| 26 |
-
from cis6270.nets import TwoTimeMLP, count_parameters
|
| 27 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed, train
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
torch.set_num_threads(1) # These networks are small, and splitting one matrix product costs more
|
| 31 |
-
# than it saves. ``tests/conftest.py`` sets the same thing for the test session, so the line
|
| 32 |
-
# here is the one that acts when a reader runs this script directly.
|
| 33 |
-
|
| 34 |
-
BOOK_BIRTHS = (0.0, 0.0, 0.35, 0.7) # The insertion times of the chapter's four-tile figure.
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
# %% 02. Local clocks
|
| 38 |
-
#
|
| 39 |
-
# A coordinate inserted at t_ins has 1 - t_ins of global time left, so its own clock runs at
|
| 40 |
-
# 1/(1 - t_ins) and reaches one exactly at the data end. Before its insertion the clock reads
|
| 41 |
-
# zero and the coordinate holds its source value.
|
| 42 |
-
def local_time(t, birth):
|
| 43 |
-
"""t_i = (t - t_ins)/(1 - t_ins), clamped to [0, 1] so an unborn coordinate reads zero."""
|
| 44 |
-
return ((t - birth) / (1.0 - birth).clamp(min=1e-6)).clamp(0.0, 1.0)
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def worked_clocks(t: float = 0.8):
|
| 48 |
-
"""The clocks, the speeds and the interpolant shares of the chapter's figure at one time."""
|
| 49 |
-
births = torch.tensor(BOOK_BIRTHS)
|
| 50 |
-
clocks = local_time(torch.tensor(t), births) # [4].
|
| 51 |
-
speeds = 1.0 / (1.0 - births) # [4] dt_i/dt.
|
| 52 |
-
return {
|
| 53 |
-
"insertion times": [round(v, 2) for v in births.tolist()],
|
| 54 |
-
"local clocks at t=0.8": [round(v, 3) for v in clocks.tolist()],
|
| 55 |
-
"clock speeds": [round(v, 3) for v in speeds.tolist()],
|
| 56 |
-
"source share": [round(v, 3) for v in (1.0 - clocks).tolist()],
|
| 57 |
-
"last clock from its speed": float(speeds[-1] * (1.0 - t)),
|
| 58 |
-
"live coordinates at 0.8": int((births <= t).sum()),
|
| 59 |
-
"live coordinates at 0.6": int((births <= 0.6).sum()),
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
# %% 03. The composition weight under growth
|
| 64 |
-
#
|
| 65 |
-
# The convexity weight is a ratio of remaining times and makes no reference to the dimension, so
|
| 66 |
-
# the derivation at fixed length goes through once every leg has been lifted to the final length.
|
| 67 |
-
# A coordinate born at zero carries the chapter's omega; a later birth carries its own.
|
| 68 |
-
def omega(s, u, t):
|
| 69 |
-
"""omega_{s,u,t} = (u-s)(1-t)/((t-s)(1-u)), the share of the earlier leg."""
|
| 70 |
-
return (u - s) * (1.0 - t) / (((t - s) * (1.0 - u)).clamp(min=1e-9))
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def worked_omega(s: float = 0.3, u: float = 0.6, t: float = 0.9):
|
| 74 |
-
"""The weight at the chapter's triple, for coordinates born at three different times."""
|
| 75 |
-
out = {"omega at (0.3, 0.6, 0.9)": float(omega(*(torch.tensor([v]) for v in (s, u, t))))}
|
| 76 |
-
for birth in (0.0, 0.35, 0.7):
|
| 77 |
-
b = torch.tensor([birth])
|
| 78 |
-
local = [local_time(torch.tensor([v]), b) for v in (s, u, t)]
|
| 79 |
-
out[f"omega for a birth at {birth}"] = float(omega(*local))
|
| 80 |
-
return out
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
# %% 04. A state whose length grows, and where the fresh noise enters
|
| 84 |
-
#
|
| 85 |
-
# The source values of every coordinate that will ever appear are drawn once, at the final
|
| 86 |
-
# length, and a leg that stops before an insertion leaves that coordinate at its source value.
|
| 87 |
-
# One draw then serves all three maps of the composition identity.
|
| 88 |
-
def lifted_state(source, data_onehot, t, births):
|
| 89 |
-
"""The state at global time t, each coordinate interpolated on its own clock. [B, L, K]."""
|
| 90 |
-
clock = local_time(t[:, None], births[None, :])[:, :, None] # [B, L, 1].
|
| 91 |
-
return (1.0 - clock) * source + clock * data_onehot # [B, L, K].
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def expansion_log_density(source, births, s: float, t: float):
|
| 95 |
-
"""The source log density of the coordinates inserted inside (s, t], the expansion term."""
|
| 96 |
-
inserted = (births > s) & (births <= t) # [L] which coordinates appear on this interval.
|
| 97 |
-
per_coordinate = (-0.5 * source ** 2 - 0.5 * math.log(2.0 * math.pi)).sum(-1) # [B, L].
|
| 98 |
-
return float(per_coordinate[:, inserted].sum(-1).mean()), int(inserted.sum())
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
# %% 05. The model, a two-time denoiser that also reads each coordinate's clock
|
| 102 |
-
def predict(model, x, s, t, births, length, vocab):
|
| 103 |
-
"""psi_{s,t}(x) at every position, with each coordinate's own clock as an extra channel.
|
| 104 |
-
|
| 105 |
-
The network has the same width in and out, so every position carries K + 1 channels, the K
|
| 106 |
-
coordinates of the state and the local clock. The clock channel of the output is dropped.
|
| 107 |
-
"""
|
| 108 |
-
clock = local_time(s[:, None], births[None, :])[:, :, None] # [B, L, 1] where each stands.
|
| 109 |
-
features = torch.cat([x, clock], dim=-1).flatten(1) # [B, L*(K+1)].
|
| 110 |
-
logits = model(features, s, t).view(-1, length, vocab + 1)[..., :vocab] # [B, L, K].
|
| 111 |
-
return torch.softmax(logits, dim=-1) # [B, L, K] on the simplex at every position.
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def step_map(x, prediction, s, t, births):
|
| 115 |
-
"""The convex form, with the weights read on each coordinate's own clock."""
|
| 116 |
-
s_local = local_time(s[:, None], births[None, :])[:, :, None] # [B, L, 1].
|
| 117 |
-
t_local = local_time(t[:, None], births[None, :])[:, :, None] # [B, L, 1].
|
| 118 |
-
keep = (1.0 - t_local) / (1.0 - s_local).clamp(min=1e-6)
|
| 119 |
-
return keep * x + (1.0 - keep) * prediction # [B, L, K].
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def make_loss(model, data, births, length, vocab, batch_size, device):
|
| 123 |
-
eye = torch.eye(vocab, device=device)
|
| 124 |
-
|
| 125 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 126 |
-
index = torch.randint(0, len(data), (batch_size,), device=device)
|
| 127 |
-
ids = data[index] # [B, L].
|
| 128 |
-
x1 = eye[ids] # [B, L, K] the data, lifted.
|
| 129 |
-
source = torch.randn(batch_size, length, vocab, device=device) # Drawn once, at full length.
|
| 130 |
-
t_diag = torch.rand(batch_size, device=device)
|
| 131 |
-
xt = lifted_state(source, x1, t_diag, births)
|
| 132 |
-
diagonal = -(predict(model, xt, t_diag, t_diag, births, length, vocab).clamp_min(1e-9)
|
| 133 |
-
.log().gather(-1, ids[..., None])).mean() # Cross-entropy on the diagonal.
|
| 134 |
-
|
| 135 |
-
s = torch.rand(batch_size, device=device)
|
| 136 |
-
t = s + (1.0 - s) * torch.rand(batch_size, device=device)
|
| 137 |
-
u = s + (t - s) * torch.rand(batch_size, device=device)
|
| 138 |
-
xs = lifted_state(source, x1, s, births) # The same source draw serves all three legs.
|
| 139 |
-
with torch.no_grad():
|
| 140 |
-
first = predict(model, xs, s, u, births, length, vocab)
|
| 141 |
-
second = predict(model, step_map(xs, first, s, u, births), u, t, births, length, vocab)
|
| 142 |
-
s_l, u_l, t_l = (local_time(v[:, None], births[None, :]) for v in (s, u, t))
|
| 143 |
-
weight = omega(s_l, u_l, t_l)[:, :, None] # [B, L, 1] one weight per coordinate.
|
| 144 |
-
target = weight * first + (1.0 - weight) * second # Still on the simplex.
|
| 145 |
-
student = predict(model, xs, s, t, births, length, vocab).clamp_min(1e-9)
|
| 146 |
-
return diagonal + (target * (target.clamp_min(1e-9) / student).log()).sum(-1).mean()
|
| 147 |
-
|
| 148 |
-
return loss_fn
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
# %% 06. Sampling
|
| 152 |
-
@torch.no_grad()
|
| 153 |
-
def sample(model, count, births, length, vocab, steps, device):
|
| 154 |
-
"""Walk a uniform grid, with every coordinate stepping on its own clock."""
|
| 155 |
-
x = torch.randn(count, length, vocab, device=device) # The source values, drawn once.
|
| 156 |
-
grid = torch.linspace(0.0, 1.0, steps + 1)
|
| 157 |
-
for a, b in zip(grid[:-1], grid[1:]):
|
| 158 |
-
s, t = a.expand(count).to(device), b.expand(count).to(device)
|
| 159 |
-
x = step_map(x, predict(model, x, s, t, births, length, vocab), s, t, births)
|
| 160 |
-
return x.argmax(-1) # [N, L].
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
# %% 07. The report
|
| 164 |
-
def main() -> None:
|
| 165 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 166 |
-
parser.set_defaults(steps=1500, batch_size=128, samples=64, length=16, sample_steps=4)
|
| 167 |
-
args = parser.parse_args()
|
| 168 |
-
set_seed(args.seed)
|
| 169 |
-
device = pick_device(args.device)
|
| 170 |
-
tokens, _ = synthetic_dna(num_sequences=1024, length=args.length, seed=args.seed)
|
| 171 |
-
data = tokens.to(device) # [1024, L] the full-length sequences.
|
| 172 |
-
|
| 173 |
-
# Half the coordinates exist at the source; the rest are inserted at evenly spaced times.
|
| 174 |
-
half = args.length // 2
|
| 175 |
-
births = torch.cat([torch.zeros(half, device=device),
|
| 176 |
-
torch.linspace(0.1, 0.8, args.length - half, device=device)]) # [L].
|
| 177 |
-
|
| 178 |
-
report = worked_clocks()
|
| 179 |
-
report.update(worked_omega())
|
| 180 |
-
report["coordinates at the source"] = int((births == 0).sum())
|
| 181 |
-
report["coordinates at the data"] = args.length
|
| 182 |
-
source = torch.randn(256, args.length, DNA_SIZE)
|
| 183 |
-
added, count = expansion_log_density(source, births.cpu(), 0.0, 0.5)
|
| 184 |
-
report["coordinates inserted in (0, 0.5]"] = count
|
| 185 |
-
report["source log density of those"] = added
|
| 186 |
-
if not args.quiet:
|
| 187 |
-
print(f"local clocks at t = 0.8: {report['local clocks at t=0.8']} with speeds "
|
| 188 |
-
f"{report['clock speeds']}")
|
| 189 |
-
print(f"omega at (0.3, 0.6, 0.9) = {report['omega at (0.3, 0.6, 0.9)']:.3f}")
|
| 190 |
-
|
| 191 |
-
model = TwoTimeMLP(dim=args.length * (DNA_SIZE + 1), width=args.width).to(device)
|
| 192 |
-
if not args.quiet:
|
| 193 |
-
print(f"\ntraining the expanding map parameters={count_parameters(model):,}")
|
| 194 |
-
losses = train(model, make_loss(model, data, births, args.length, DNA_SIZE, args.batch_size,
|
| 195 |
-
device), steps=args.steps, lr=args.lr, quiet=args.quiet)
|
| 196 |
-
|
| 197 |
-
samples: list[str] = []
|
| 198 |
-
early, late = births == 0, births > 0 # Coordinates born at the source and born later.
|
| 199 |
-
for steps in (1, args.sample_steps):
|
| 200 |
-
torch.manual_seed(args.seed + 1)
|
| 201 |
-
drawn = sample(model, args.samples, births, args.length, DNA_SIZE, steps, device)
|
| 202 |
-
report[f"{steps}-step GC fraction"] = gc_fraction(drawn)
|
| 203 |
-
report[f"{steps}-step GC, born at the source"] = gc_fraction(drawn[:, early])
|
| 204 |
-
report[f"{steps}-step GC, born later"] = gc_fraction(drawn[:, late])
|
| 205 |
-
report[f"{steps}-step motif fraction"] = motif_fraction(drawn)
|
| 206 |
-
report[f"{steps}-step token entropy"] = token_entropy(drawn, DNA_SIZE)
|
| 207 |
-
report[f"{steps}-step unique fraction"] = unique_fraction(drawn)
|
| 208 |
-
samples.append(f"{steps} step(s): {decode_dna(drawn[:1])[0]} GC {gc_fraction(drawn):.3f}")
|
| 209 |
-
report["training GC fraction"] = gc_fraction(data)
|
| 210 |
-
report["final loss"] = sum(losses[-20:]) / len(losses[-20:])
|
| 211 |
-
|
| 212 |
-
print_report("Expanding flow maps, Chapter 6", report, samples)
|
| 213 |
-
save_run(args.out, model=model, config=vars(args), report=report, losses=losses, samples=samples)
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
if __name__ == "__main__":
|
| 217 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_6/flow_map.py
DELETED
|
@@ -1,206 +0,0 @@
|
|
| 1 |
-
"""The flow map of an ordinary differential equation, with no learning anywhere.
|
| 2 |
-
|
| 3 |
-
A flow map X_{s,t}(x) returns the state at time t of the trajectory that passes through x at time
|
| 4 |
-
s, so one evaluation replaces the N Euler steps of Chapter 2. Here we integrate a velocity we
|
| 5 |
-
already know, the Gaussian flow of the chapter, and check against it every property the rest of
|
| 6 |
-
the lecture trains for: the three axioms, the inverse, the Jacobian and its differential equation,
|
| 7 |
-
Liouville's formula, the transport of a density, and the generator.
|
| 8 |
-
|
| 9 |
-
Chapter 6, Sections 6.3 to 6.12 of the notes. Runs on the scalar Gaussian flow and on a batch from
|
| 10 |
-
``cis6270.data.gaussian_mixture_2d``. Prints the four numbers of the chapter's listing, 4.100000
|
| 11 |
-
for the map, the cocycle agreeing with it, 0.700000 for the inverse and a Jacobian determinant of
|
| 12 |
-
3.000000, together with log 3, the density ratio and the log likelihood.
|
| 13 |
-
|
| 14 |
-
python lecture_6/flow_map.py # 200 Runge-Kutta substeps per crossing
|
| 15 |
-
python lecture_6/flow_map.py --steps 20 # 20 substeps, a second, and the same report
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
import math
|
| 20 |
-
|
| 21 |
-
import torch
|
| 22 |
-
from torch.func import jacrev # Differentiates the whole integration in its starting point.
|
| 23 |
-
|
| 24 |
-
from cis6270.data import GAUSSIAN_STD, gaussian_mixture_2d
|
| 25 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 26 |
-
|
| 27 |
-
torch.set_num_threads(1) # The integration is a scalar loop over a small batch, so one thread is
|
| 28 |
-
# the fastest here. ``tests/conftest.py`` sets the same thing for the test session, so the line here
|
| 29 |
-
# is the one that acts when a reader runs this script directly.
|
| 30 |
-
torch.set_default_dtype(torch.float64) # The axioms are checked below the float32 noise floor.
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
SUBSTEPS = 200 # n, the Runge-Kutta substeps per crossing. No optimizer runs in this file.
|
| 34 |
-
START = 0.7 # The state x at which the chapter evaluates every check.
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
# %% 02. The velocity field of the Gaussian flow
|
| 38 |
-
#
|
| 39 |
-
# Source N(0,1) and data N(2,9), paired by x_1 = 2 + 3 x_0 and joined by a straight line. The
|
| 40 |
-
# velocity is a translation at speed 2 plus an expansion whose strength 2/(1+2t) decays as the
|
| 41 |
-
# cloud widens.
|
| 42 |
-
def b(x, t): # velocity field
|
| 43 |
-
return 2.0 + 2.0 * (x - 2.0 * t) / (1.0 + 2.0 * t) # Section 6.9, b(x,t).
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def exact_map(x, s, t):
|
| 47 |
-
"""X_{s,t}(x) = 2t + (1+2t)(x - 2s)/(1 + 2s), the closed form the integrator has to reproduce."""
|
| 48 |
-
return 2.0 * t + (1.0 + 2.0 * t) * (x - 2.0 * s) / (1.0 + 2.0 * s) # [B] or scalar.
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
# %% 03. The flow map by fourth-order Runge-Kutta
|
| 52 |
-
#
|
| 53 |
-
# Four velocity evaluations per substep, at its start, twice at its midpoint and at its end,
|
| 54 |
-
# combined with the weights 1, 2, 2, 1 over six. The step h is negative when t is below s, so the
|
| 55 |
-
# same function integrates backward with no change.
|
| 56 |
-
def flow(x, s, t, n=SUBSTEPS): # X_{s,t}(x) by RK4
|
| 57 |
-
h = (t - s) / n
|
| 58 |
-
for k in range(n):
|
| 59 |
-
u = s + k * h
|
| 60 |
-
k1 = b(x, u)
|
| 61 |
-
k2 = b(x + 0.5 * h * k1, u + 0.5 * h)
|
| 62 |
-
k3 = b(x + 0.5 * h * k2, u + 0.5 * h)
|
| 63 |
-
k4 = b(x + h * k3, u + h)
|
| 64 |
-
x = x + (h / 6.0) * (k1 + 2 * k2 + 2 * k3 + k4)
|
| 65 |
-
return x
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
# %% 04. The three axioms, checked numerically
|
| 69 |
-
#
|
| 70 |
-
# Identity, cocycle and tangent characterize the map (Proposition 6.1). The first holds by
|
| 71 |
-
# definition, the second by uniqueness of the trajectory, the third by the defining equation.
|
| 72 |
-
def check_axioms(x, n):
|
| 73 |
-
"""Return the residual of each axiom at the state x, each of which the true map drives to zero."""
|
| 74 |
-
identity = flow(x, 0.5, 0.5, n) - x # X_{t,t} = id.
|
| 75 |
-
cocycle = flow(flow(x, 0.0, 0.5, n), 0.5, 1.0, n) - flow(x, 0.0, 1.0, n) # X_{t,u} o X_{s,t}.
|
| 76 |
-
h = 1e-6 # One-sided difference quotient at the diagonal.
|
| 77 |
-
tangent = (flow(x, 0.5, 0.5 + h, n) - x) / h - b(x, 0.5) # d/dt X at t = s equals b(x,s).
|
| 78 |
-
return float(identity.abs()), float(cocycle.abs()), float(tangent.abs())
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
# %% 05. The Jacobian and the equation it satisfies
|
| 82 |
-
#
|
| 83 |
-
# dJ/dt = grad b(X_{s,t}(x), t) J, starting from J_{s,s} = I. In one dimension grad b is the single
|
| 84 |
-
# derivative 2/(1+2t), and the solution from zero is J_{0,t} = 1 + 2t.
|
| 85 |
-
def jacobian_by_ode(x, s, t, n):
|
| 86 |
-
"""Integrate the state and its Jacobian together, with one Euler step each on the same grid."""
|
| 87 |
-
jacobian = torch.ones_like(x) # J_{s,s} = I, the identity axiom differentiated in x.
|
| 88 |
-
h = (t - s) / n
|
| 89 |
-
for k in range(n):
|
| 90 |
-
u = s + k * h
|
| 91 |
-
grad_b = 2.0 / (1.0 + 2.0 * u) # d b / d x at time u, the divergence in one dimension.
|
| 92 |
-
jacobian = jacobian + h * grad_b * jacobian # Linear in J, with the coefficient frozen.
|
| 93 |
-
x = x + h * b(x, u) # The trajectory the coefficient is read along.
|
| 94 |
-
return jacobian
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def liouville_log_det(s, t, n):
|
| 98 |
-
"""Integrate d/dt log det J = div b, which on this flow is the number log 3 over [0, 1]."""
|
| 99 |
-
h = (t - s) / n
|
| 100 |
-
grid = s + h * (torch.arange(n) + 0.5) # Midpoints of the substeps.
|
| 101 |
-
return float((2.0 / (1.0 + 2.0 * grid)).sum() * h) # Riemann sum of the divergence.
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
# %% 06. Transporting a density along the flow
|
| 105 |
-
#
|
| 106 |
-
# p_t(X_{s,t}(x)) |det J_{s,t}(x)| = p_s(x). The flow stretches every length by 3 over the unit
|
| 107 |
-
# interval, so it divides every density by 3.
|
| 108 |
-
def normal_density(x, mean, std):
|
| 109 |
-
"""The scalar Gaussian density, used at both ends of the transport statement."""
|
| 110 |
-
return math.exp(-((x - mean) ** 2) / (2.0 * std ** 2)) / (std * math.sqrt(2.0 * math.pi))
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def mixture_log_density_2d(points, scale, shift):
|
| 114 |
-
"""Log density of the four-Gaussian mixture after the affine map y = shift + scale * x.
|
| 115 |
-
|
| 116 |
-
The pushforward of a Gaussian mixture under an affine map is a Gaussian mixture with the
|
| 117 |
-
centers and the widths carried through, which gives the transported density in closed form.
|
| 118 |
-
"""
|
| 119 |
-
from cis6270.data import GAUSSIAN_CENTERS
|
| 120 |
-
|
| 121 |
-
centers = shift + scale * GAUSSIAN_CENTERS.to(points) # [4, 2] transported centers.
|
| 122 |
-
std = scale * GAUSSIAN_STD # Each component is widened by the same factor.
|
| 123 |
-
squared = ((points[:, None, :] - centers) ** 2).sum(dim=-1) # [N, 4].
|
| 124 |
-
component = -0.5 * squared / std ** 2 - math.log(2.0 * math.pi * std ** 2) # [N, 4].
|
| 125 |
-
return torch.logsumexp(component + math.log(0.25), dim=-1) # [N] mixture log density.
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
# %% 07. The generator of the flow
|
| 129 |
-
#
|
| 130 |
-
# (L_t f)(x) = b(x,t) . grad f(x), and the rate of change of f along a trajectory is the generator
|
| 131 |
-
# applied to f and read at the moving point.
|
| 132 |
-
def generator(f, grad_f, x, t):
|
| 133 |
-
"""(L_t f)(x) for a scalar test function, which in one dimension is b times the derivative."""
|
| 134 |
-
return b(x, t) * grad_f(x) # Section 6.11, the generator.
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
# %% 08. The report
|
| 138 |
-
def main() -> None:
|
| 139 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 140 |
-
parser.set_defaults(steps=SUBSTEPS, samples=512)
|
| 141 |
-
args = parser.parse_args() # --steps is the substep count n here, since nothing is trained.
|
| 142 |
-
set_seed(args.seed)
|
| 143 |
-
n = max(args.steps, 4) # Runge-Kutta needs a handful of substeps to be worth its name.
|
| 144 |
-
|
| 145 |
-
# %% 09. The chapter's four lines
|
| 146 |
-
x = torch.tensor(START) # The single starting point every check below uses.
|
| 147 |
-
one_crossing = flow(x, 0.0, 1.0, n) # 2 + 3x = 4.1.
|
| 148 |
-
two_crossings = flow(flow(x, 0.0, 0.5, n), 0.5, 1.0, n) # The cocycle.
|
| 149 |
-
there_and_back = flow(flow(x, 0.0, 1.0, n), 1.0, 0.0, n) # The inverse.
|
| 150 |
-
det_jacobian = jacrev(lambda z: flow(z, 0.0, 1.0, n))(x) # det J = 3.
|
| 151 |
-
if not args.quiet:
|
| 152 |
-
print(f"X_0,1({START}) = {float(one_crossing):.6f} closed form "
|
| 153 |
-
f"{float(exact_map(x, 0.0, 1.0)):.6f}")
|
| 154 |
-
print(f"cocycle through 0.5 = {float(two_crossings):.6f}")
|
| 155 |
-
print(f"inverse X_1,0 o X_0,1 = {float(there_and_back):.6f}")
|
| 156 |
-
print(f"det J_0,1 = {float(det_jacobian):.6f}")
|
| 157 |
-
|
| 158 |
-
identity, cocycle, tangent = check_axioms(x, n)
|
| 159 |
-
|
| 160 |
-
# %% 10. The Jacobian two ways, and Liouville's formula
|
| 161 |
-
jacobian_ode = jacobian_by_ode(x, 0.0, 1.0, 4000) # One Euler solve of dJ/dt = grad b . J.
|
| 162 |
-
log_det = liouville_log_det(0.0, 1.0, 4000) # Integral of the divergence over [0, 1].
|
| 163 |
-
|
| 164 |
-
# %% 11. Transport, in one dimension and in two
|
| 165 |
-
image = float(one_crossing) # X_{0,1}(0.7) = 4.1.
|
| 166 |
-
p_source = normal_density(START, 0.0, 1.0) # p_0(0.7).
|
| 167 |
-
p_data = normal_density(image, 2.0, 3.0) # p_1(4.1).
|
| 168 |
-
log_likelihood = math.log(p_source) - log_det # Section 6.10, with the divergence integral.
|
| 169 |
-
|
| 170 |
-
points = gaussian_mixture_2d(num_points=args.samples, seed=args.seed) # [N, 2] source cloud.
|
| 171 |
-
moved = exact_map(points, 0.0, 1.0) # The same affine flow applied to both coordinates.
|
| 172 |
-
log_p0 = mixture_log_density_2d(points, 1.0, 0.0) # [N] density before transport.
|
| 173 |
-
log_p1 = mixture_log_density_2d(moved, 3.0, 2.0) # [N] density of the transported mixture.
|
| 174 |
-
transport_gap = float((log_p1 + 2.0 * math.log(3.0) - log_p0).abs().max()) # det J = 3^2 = 9.
|
| 175 |
-
|
| 176 |
-
# %% 12. The generator along one trajectory
|
| 177 |
-
f, grad_f = (lambda z: z ** 2), (lambda z: 2.0 * z) # A test function and its derivative.
|
| 178 |
-
h = 1e-5
|
| 179 |
-
along = (f(flow(x, 0.0, 0.5 + h, n)) - f(flow(x, 0.0, 0.5 - h, n))) / (2.0 * h) # d/dt f(X).
|
| 180 |
-
generator_gap = abs(float(along) - float(generator(f, grad_f, flow(x, 0.0, 0.5, n), 0.5)))
|
| 181 |
-
|
| 182 |
-
report = {
|
| 183 |
-
"X_0,1(0.7)": float(one_crossing),
|
| 184 |
-
"cocycle through u=0.5": float(two_crossings),
|
| 185 |
-
"inverse back to the start": float(there_and_back),
|
| 186 |
-
"det J_0,1 by jacrev": float(det_jacobian),
|
| 187 |
-
"det J_0,1 by its own ODE": float(jacobian_ode),
|
| 188 |
-
"log det J_0,1 by Liouville": log_det,
|
| 189 |
-
"identity axiom residual": identity,
|
| 190 |
-
"cocycle axiom residual": cocycle,
|
| 191 |
-
"tangent axiom residual": tangent,
|
| 192 |
-
"p_0(0.7)": p_source,
|
| 193 |
-
"p_1(4.1)": p_data,
|
| 194 |
-
"density ratio p_0/p_1": p_source / p_data,
|
| 195 |
-
"log p_1(4.1) by transport": log_likelihood,
|
| 196 |
-
"log p_1(4.1) directly": math.log(p_data),
|
| 197 |
-
"2-D transport gap": transport_gap,
|
| 198 |
-
"generator gap": generator_gap,
|
| 199 |
-
"substeps n": n,
|
| 200 |
-
}
|
| 201 |
-
print_report("Flow map of the Gaussian flow, Chapter 6", report)
|
| 202 |
-
save_run(args.out, config=vars(args), report=report)
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
if __name__ == "__main__":
|
| 206 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_6/fmlm.py
DELETED
|
@@ -1,291 +0,0 @@
|
|
| 1 |
-
"""Flow Map Language Models, the composition identity in denoiser coordinates.
|
| 2 |
-
|
| 3 |
-
Each token is lifted to a one-hot vector, the source is Gaussian over that space, and the state
|
| 4 |
-
moves through R^{L x K}, so no atom has to be split. Stepping by the full remaining time 1 - s in
|
| 5 |
-
place of the elapsed time t - s turns the average velocity into a two-time denoiser that lands on
|
| 6 |
-
the simplex, the map becomes a convex combination of staying put and jumping to the prediction,
|
| 7 |
-
and the cocycle becomes a convex combination of two shorter predictions, which a KL divergence
|
| 8 |
-
can measure.
|
| 9 |
-
|
| 10 |
-
Chapter 6, Sections 6.25 to 6.27 of the notes. Trained on the synthetic DNA of
|
| 11 |
-
``cis6270.data.synthetic_dna``. Prints the convex weights, the composition weight gamma = 0.2 at
|
| 12 |
-
(0.2, 0.5, 0.8), the worked KL of 0.030747 nats, the two gradients that decide the loss, the
|
| 13 |
-
decoding clock of a 30,522-token vocabulary and the four steps it produces, and samples from the
|
| 14 |
-
trained map.
|
| 15 |
-
|
| 16 |
-
python lecture_6/fmlm.py # 1500 steps, about half a minute
|
| 17 |
-
python lecture_6/fmlm.py --steps 20 # a few seconds, for a first look
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
# %% 01. Imports and configuration
|
| 21 |
-
import math
|
| 22 |
-
|
| 23 |
-
import torch
|
| 24 |
-
|
| 25 |
-
from cis6270.data import DNA_SIZE, decode_dna, synthetic_dna
|
| 26 |
-
from cis6270.metrics import gc_fraction, motif_fraction, token_entropy, unique_fraction
|
| 27 |
-
from cis6270.nets import TwoTimeMLP, count_parameters
|
| 28 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed, train
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
torch.set_num_threads(1) # These networks are small, and splitting one matrix product costs more
|
| 32 |
-
# than it saves. ``tests/conftest.py`` sets the same thing for the test session, so the line
|
| 33 |
-
# here is the one that acts when a reader runs this script directly.
|
| 34 |
-
|
| 35 |
-
BOOK_VOCAB = 30522 # The vocabulary the chapter puts on the clock, a standard tokenizer.
|
| 36 |
-
ENDPOINT_FRACTION = 0.25 # Share of each batch drawn with t = 1, the slice the sampler reads.
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
# %% 02. The convex form and the composition weight
|
| 40 |
-
#
|
| 41 |
-
# X_{s,t}(x) = (1-t)/(1-s) x + (t-s)/(1-s) delta_{s,t}(x). Both weights are non-negative for
|
| 42 |
-
# s <= t <= 1 and they sum to one, so each step is a convex combination of staying put and jumping
|
| 43 |
-
# to the predicted endpoint. At t = s the pair is (1, 0) and the identity axiom holds.
|
| 44 |
-
def convex_weights(s, t):
|
| 45 |
-
"""The pair ((1-t)/(1-s), (t-s)/(1-s)) that the step mixes the state and the prediction with."""
|
| 46 |
-
remaining = (1.0 - s).clamp(min=1e-6)
|
| 47 |
-
return (1.0 - t) / remaining, (t - s) / remaining
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def composition_weight(s, u, t):
|
| 51 |
-
"""gamma = (1-t)(u-s)/((1-u)(t-s)), the share of the long prediction made over the first leg."""
|
| 52 |
-
return (1.0 - t) * (u - s) / (((1.0 - u) * (t - s)).clamp(min=1e-6))
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
# %% 03. The cocycle in denoiser coordinates, checked on the scalar flow
|
| 56 |
-
#
|
| 57 |
-
# delta_{s,t}(x) = gamma delta_{s,u}(x) + (1-gamma) delta_{u,t}(X_{s,u}(x)). On dx/dt = x the
|
| 58 |
-
# two-time denoiser is x[1 + (1-s)(e^{t-s}-1)/(t-s)], so every term is available in closed form.
|
| 59 |
-
def scalar_denoiser(x, s, t):
|
| 60 |
-
"""delta_{s,t}(x) on the flow dx/dt = x."""
|
| 61 |
-
return x * (1.0 + (1.0 - s) * math.expm1(t - s) / (t - s))
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def scalar_cocycle(x: float = 1.0, s: float = 0.2, u: float = 0.5, t: float = 0.8):
|
| 65 |
-
"""Every term of the denoiser cocycle at the chapter's triple of times."""
|
| 66 |
-
gamma = (1.0 - t) * (u - s) / ((1.0 - u) * (t - s))
|
| 67 |
-
first = scalar_denoiser(x, s, u) # The prediction over the first leg.
|
| 68 |
-
reached = x * math.exp(u - s) # X_{s,u}(x), the state the first leg arrives at.
|
| 69 |
-
second = scalar_denoiser(reached, u, t) # The prediction over the second leg.
|
| 70 |
-
long = scalar_denoiser(x, s, t) # The prediction over the whole interval.
|
| 71 |
-
keep, move = (1.0 - t) / (1.0 - s), (t - s) / (1.0 - s)
|
| 72 |
-
return {"gamma": gamma, "delta_s,u": first, "delta_u,t": second, "delta_s,t": long,
|
| 73 |
-
"two legs combined": gamma * first + (1.0 - gamma) * second,
|
| 74 |
-
"convex weights": (round(keep, 4), round(move, 4)),
|
| 75 |
-
"convex form": keep * x + move * long, "X_s,t(x)": x * math.exp(t - s)}
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
# %% 04. One term of the loss, on the DNA alphabet
|
| 79 |
-
#
|
| 80 |
-
# The composed target is a convex combination of two simplex points, so it is a distribution and
|
| 81 |
-
# the divergence against it needs no projection. Its gradient pushes mass onto the token with more
|
| 82 |
-
# weight in the two-leg decomposition than in the long prediction.
|
| 83 |
-
def worked_kl():
|
| 84 |
-
"""The chapter's four-token example, with the target composed from two student predictions."""
|
| 85 |
-
first = torch.tensor([0.10, 0.20, 0.60, 0.10]) # delta_{0.2,0.5} over (A, C, G, T).
|
| 86 |
-
second = torch.tensor([0.05, 0.10, 0.80, 0.05]) # delta_{0.5,0.8} at the state the leg reaches.
|
| 87 |
-
gamma = (1.0 - 0.8) * (0.5 - 0.2) / ((1.0 - 0.5) * (0.8 - 0.2))
|
| 88 |
-
target = gamma * first + (1.0 - gamma) * second # [4] the composed target.
|
| 89 |
-
student = torch.tensor([0.10, 0.15, 0.65, 0.10]) # The student's long prediction.
|
| 90 |
-
terms = target * (target / student).log() # [4] the four terms of the divergence.
|
| 91 |
-
return {"worked gamma": gamma, "composed target": [round(v, 4) for v in target.tolist()],
|
| 92 |
-
"target mass": float(target.sum()), "worked KL": float(terms.sum()),
|
| 93 |
-
"the four terms": [round(v, 6) for v in terms.tolist()]}
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
# %% 05. The two gradients a softmax head sees
|
| 97 |
-
#
|
| 98 |
-
# With a softmax head the KL gradient in the logits is q - p, so the size of the correction in
|
| 99 |
-
# coordinate i is the error in coordinate i. The squared error passes through the softmax Jacobian
|
| 100 |
-
# q_j(delta_ij - q_i) and carries a factor q_i in every term, so a token the model has learned to
|
| 101 |
-
# rule out gets almost no correction. Both gradients are taken by automatic differentiation here,
|
| 102 |
-
# which checks the two expressions of Section 6.27 as well as evaluating them.
|
| 103 |
-
def logit_gradients(target, student):
|
| 104 |
-
"""The gradient of KL(p || q) and of half the squared error in the logits of q. Two [K]."""
|
| 105 |
-
for_kl = student.log().clone().requires_grad_(True) # Logits whose softmax is the student.
|
| 106 |
-
divergence = (target * (target / torch.softmax(for_kl, -1)).log()).sum()
|
| 107 |
-
kl_gradient, = torch.autograd.grad(divergence, for_kl) # [K] equal to q - p.
|
| 108 |
-
for_squared = student.log().clone().requires_grad_(True) # A second graph on the same logits.
|
| 109 |
-
squared = 0.5 * ((torch.softmax(for_squared, -1) - target) ** 2).sum()
|
| 110 |
-
squared_gradient, = torch.autograd.grad(squared, for_squared) # [K] with a q_i throughout.
|
| 111 |
-
return kl_gradient, squared_gradient
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def gradient_comparison(floor: float = 1e-6):
|
| 115 |
-
"""The two gradients at the chapter's student, and again once that student has saturated."""
|
| 116 |
-
target = torch.tensor([0.06, 0.12, 0.76, 0.06]) # p, the composed target of Section 6.27.
|
| 117 |
-
student = torch.tensor([0.10, 0.15, 0.65, 0.10]) # q, the student's long prediction.
|
| 118 |
-
kl, squared = logit_gradients(target, student)
|
| 119 |
-
ratio = (squared / kl).abs() # Between a quarter and a half, coordinate by coordinate.
|
| 120 |
-
rest = student[1:] * (1.0 - floor) / student[1:].sum() # The other three keep their shares.
|
| 121 |
-
saturated = torch.cat([torch.full_like(student[:1], floor), rest]) # [K], summing to one.
|
| 122 |
-
kl_low, squared_low = logit_gradients(target, saturated)
|
| 123 |
-
return {
|
| 124 |
-
"KL gradient": [round(v, 5) for v in kl.tolist()],
|
| 125 |
-
"squared-error gradient": [round(v, 5) for v in squared.tolist()],
|
| 126 |
-
"squared over KL": [round(v, 4) for v in ratio.tolist()],
|
| 127 |
-
"saturated student": [round(v, 6) for v in saturated.tolist()],
|
| 128 |
-
"saturated KL gradient": float(kl_low[0]),
|
| 129 |
-
# The surviving squared-error gradient is of order 1e-08, which four printed decimals
|
| 130 |
-
# cannot show, so the report carries it in units of 1e-08.
|
| 131 |
-
"saturated squared-error gradient, in 1e-08": 1e8 * float(squared_low[0]),
|
| 132 |
-
"orders of magnitude between them": math.log10(
|
| 133 |
-
abs(float(kl_low[0])) / abs(float(squared_low[0]))),
|
| 134 |
-
}
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
# %% 06. The decoding clock
|
| 138 |
-
#
|
| 139 |
-
# A position decodes correctly when its data coordinate is the largest of the |V| coordinates,
|
| 140 |
-
# a shifted standard normal against the maximum of the rest. The reparameterized time tau is the
|
| 141 |
-
# fraction of the initial decoding error already removed, and it is the clock the sampler uses.
|
| 142 |
-
def decoding_error(t: float, vocab_size: int, nodes: int = 4001) -> float:
|
| 143 |
-
"""P_e(t), the probability that one position decodes to the wrong token at time t."""
|
| 144 |
-
if t >= 1.0:
|
| 145 |
-
return 0.0
|
| 146 |
-
shift = t / (1.0 - t) # a = t/(1-t), the gap the data coordinate opens.
|
| 147 |
-
z = torch.linspace(-12.0, 14.0, nodes, dtype=torch.float64) # Quadrature grid over the draw.
|
| 148 |
-
density = torch.exp(-0.5 * z * z) / math.sqrt(2.0 * math.pi) # phi(z).
|
| 149 |
-
cumulative = 0.5 * torch.erfc(-(z + shift) / math.sqrt(2.0)) # Phi(z + a).
|
| 150 |
-
log_all = (vocab_size - 1) * cumulative.clamp(min=1e-300).log() # All the others fall short.
|
| 151 |
-
return 1.0 - float(torch.trapz(density * log_all.exp(), z))
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
def clock(vocab_size: int):
|
| 155 |
-
"""tau(t) = (P_e(0) - P_e(t))/P_e(0), as a callable, with the source error rate beside it."""
|
| 156 |
-
start = decoding_error(0.0, vocab_size)
|
| 157 |
-
return (lambda t: (start - decoding_error(t, vocab_size)) / start), start
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
def even_in_tau(vocab_size: int, pieces: int = 4):
|
| 161 |
-
"""Solve tau(t) = k/pieces by bisection and return the grid with its two endpoints."""
|
| 162 |
-
tau, _ = clock(vocab_size)
|
| 163 |
-
grid = [0.0]
|
| 164 |
-
for k in range(1, pieces):
|
| 165 |
-
low, high = 0.0, 1.0 - 1e-6
|
| 166 |
-
for _ in range(60): # Bisection; tau is increasing in t.
|
| 167 |
-
middle = 0.5 * (low + high)
|
| 168 |
-
low, high = (middle, high) if tau(middle) < k / pieces else (low, middle)
|
| 169 |
-
grid.append(0.5 * (low + high))
|
| 170 |
-
return grid + [1.0]
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
# %% 07. The model, a two-time denoiser with a softmax head
|
| 174 |
-
def predict(model, x, s, t, length, vocab):
|
| 175 |
-
"""delta_{s,t}(x) at every position, a point of the simplex produced by a softmax head."""
|
| 176 |
-
logits = model(x.flatten(1), s, t).view(-1, length, vocab) # [B, L, K] the network output.
|
| 177 |
-
return torch.softmax(logits, dim=-1) # [B, L, K] on the simplex at every position.
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
def step_map(x, prediction, s, t):
|
| 181 |
-
"""The convex form, which is the flow map written in denoiser coordinates."""
|
| 182 |
-
keep, move = convex_weights(s, t)
|
| 183 |
-
return keep[:, None, None] * x + move[:, None, None] * prediction # [B, L, K].
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
def make_loss(model, data, length, vocab, batch_size, device):
|
| 187 |
-
eye = torch.eye(vocab, device=device) # The one-hot lift of the alphabet.
|
| 188 |
-
|
| 189 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 190 |
-
index = torch.randint(0, len(data), (batch_size,), device=device)
|
| 191 |
-
x1 = eye[data[index]] # [B, L, K] the data, lifted.
|
| 192 |
-
x0 = torch.randn_like(x1) # [B, L, K] the Gaussian source.
|
| 193 |
-
t_diag = torch.rand(batch_size, device=device) # [B] a time for the diagonal term.
|
| 194 |
-
xt = (1 - t_diag[:, None, None]) * x0 + t_diag[:, None, None] * x1 # [B, L, K].
|
| 195 |
-
diagonal = -(predict(model, xt, t_diag, t_diag, length, vocab).clamp_min(1e-9).log()
|
| 196 |
-
.gather(-1, data[index][..., None])).mean() # Cross-entropy to the token.
|
| 197 |
-
|
| 198 |
-
s = torch.rand(batch_size, device=device) # [B] start time.
|
| 199 |
-
t = s + (1.0 - s) * torch.rand(batch_size, device=device) # [B] end time.
|
| 200 |
-
# The slice t = 1 is the one-step case the sampler reads, and a continuous draw never
|
| 201 |
-
# lands on it, so a fixed share of the batch is set there on purpose (Section 6.26).
|
| 202 |
-
endpoint = torch.rand(batch_size, device=device) < ENDPOINT_FRACTION # [B] which rows.
|
| 203 |
-
t = torch.where(endpoint, torch.ones_like(t), t)
|
| 204 |
-
u = s + (t - s) * torch.rand(batch_size, device=device) # [B] the split point.
|
| 205 |
-
xs = (1 - s[:, None, None]) * x0 + s[:, None, None] * x1 # [B, L, K] the interpolant.
|
| 206 |
-
with torch.no_grad(): # Both legs and the intermediate state are frozen targets.
|
| 207 |
-
first = predict(model, xs, s, u, length, vocab)
|
| 208 |
-
second = predict(model, step_map(xs, first, s, u), u, t, length, vocab)
|
| 209 |
-
gamma = composition_weight(s, u, t)[:, None, None]
|
| 210 |
-
target = gamma * first + (1.0 - gamma) * second # [B, L, K], still on the simplex.
|
| 211 |
-
student = predict(model, xs, s, t, length, vocab).clamp_min(1e-9)
|
| 212 |
-
off_diagonal = (target * (target.clamp_min(1e-9) / student).log()).sum(-1).mean()
|
| 213 |
-
return diagonal + off_diagonal
|
| 214 |
-
|
| 215 |
-
return loss_fn
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
# %% 08. Sampling on a grid of times
|
| 219 |
-
@torch.no_grad()
|
| 220 |
-
def sample(model, count, length, vocab, grid, device):
|
| 221 |
-
"""Walk the grid with the convex form, and read the alphabet off the last prediction."""
|
| 222 |
-
x = torch.randn(count, length, vocab, device=device) # [N, L, K] the Gaussian source.
|
| 223 |
-
for a, b in zip(grid[:-1], grid[1:]):
|
| 224 |
-
s = torch.full((count,), a, device=device)
|
| 225 |
-
t = torch.full((count,), b, device=device)
|
| 226 |
-
x = step_map(x, predict(model, x, s, t, length, vocab), s, t)
|
| 227 |
-
return x.argmax(-1) # [N, L] token ids, read by an argmax at every position.
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
# %% 09. The report
|
| 231 |
-
def main() -> None:
|
| 232 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 233 |
-
parser.set_defaults(steps=1500, batch_size=128, samples=64, length=16)
|
| 234 |
-
args = parser.parse_args()
|
| 235 |
-
set_seed(args.seed)
|
| 236 |
-
device = pick_device(args.device)
|
| 237 |
-
tokens, _ = synthetic_dna(num_sequences=1024, length=args.length, seed=args.seed)
|
| 238 |
-
data = tokens.to(device) # [1024, L] the training sequences.
|
| 239 |
-
|
| 240 |
-
report = scalar_cocycle()
|
| 241 |
-
report.update(worked_kl())
|
| 242 |
-
report.update(gradient_comparison())
|
| 243 |
-
if not args.quiet:
|
| 244 |
-
print(f"at the saturated student the KL gradient is "
|
| 245 |
-
f"{report['saturated KL gradient']:.4f} and the squared-error gradient "
|
| 246 |
-
f"{1e-8 * report['saturated squared-error gradient, in 1e-08']:.3e}")
|
| 247 |
-
book_tau, book_start = clock(BOOK_VOCAB)
|
| 248 |
-
for t in (0.60, 0.75, 0.80, 0.85):
|
| 249 |
-
report[f"tau({t:.2f}) at |V|=30522"] = book_tau(t)
|
| 250 |
-
book_grid = even_in_tau(BOOK_VOCAB)
|
| 251 |
-
report["four steps on the clock"] = [round(t, 6) for t in book_grid]
|
| 252 |
-
rounded = [round(t, 3) for t in book_grid] # The chapter quotes the grid to three decimals.
|
| 253 |
-
report["convex weights on that grid"] = [
|
| 254 |
-
(round((1 - b) / (1 - a), 4), round((b - a) / (1 - a), 4))
|
| 255 |
-
for a, b in zip(rounded[:-1], rounded[1:])]
|
| 256 |
-
report["gamma on the last three"] = float(composition_weight(
|
| 257 |
-
*(torch.tensor([v]) for v in rounded[1:4])))
|
| 258 |
-
report["gamma over the long first leg"] = float(composition_weight(
|
| 259 |
-
*(torch.tensor([v]) for v in (0.0, rounded[1], rounded[2]))))
|
| 260 |
-
report["tau on a uniform grid"] = [round(book_tau(t), 5) for t in (0.25, 0.5, 0.75)]
|
| 261 |
-
if not args.quiet:
|
| 262 |
-
print(f"clock at |V|=30522: P_e(0) = {book_start:.6f}, four steps at "
|
| 263 |
-
f"{[round(t, 3) for t in book_grid]}")
|
| 264 |
-
|
| 265 |
-
model = TwoTimeMLP(dim=args.length * DNA_SIZE, width=args.width).to(device)
|
| 266 |
-
if not args.quiet:
|
| 267 |
-
print(f"\ntraining the two-time denoiser parameters={count_parameters(model):,}")
|
| 268 |
-
losses = train(model, make_loss(model, data, args.length, DNA_SIZE, args.batch_size, device),
|
| 269 |
-
steps=args.steps, lr=args.lr, quiet=args.quiet)
|
| 270 |
-
|
| 271 |
-
dna_grid = even_in_tau(DNA_SIZE) # The same clock, read at the four-letter alphabet.
|
| 272 |
-
report["DNA clock grid"] = [round(t, 4) for t in dna_grid]
|
| 273 |
-
samples: list[str] = []
|
| 274 |
-
for name, grid in (("one step", [0.0, 1.0]), ("four, uniform", [0.0, 0.25, 0.5, 0.75, 1.0]),
|
| 275 |
-
("four, on the clock", dna_grid)):
|
| 276 |
-
torch.manual_seed(args.seed + 1) # The same source noise for every schedule.
|
| 277 |
-
drawn = sample(model, args.samples, args.length, DNA_SIZE, grid, device)
|
| 278 |
-
report[f"{name}: GC fraction"] = gc_fraction(drawn)
|
| 279 |
-
report[f"{name}: motif fraction"] = motif_fraction(drawn)
|
| 280 |
-
report[f"{name}: token entropy"] = token_entropy(drawn, DNA_SIZE)
|
| 281 |
-
report[f"{name}: unique fraction"] = unique_fraction(drawn)
|
| 282 |
-
samples.append(f"{name:<20s} {decode_dna(drawn[:1])[0]} GC {gc_fraction(drawn):.3f}")
|
| 283 |
-
report["training GC fraction"] = gc_fraction(data)
|
| 284 |
-
report["final loss"] = sum(losses[-20:]) / len(losses[-20:])
|
| 285 |
-
|
| 286 |
-
print_report("Flow Map Language Models, Chapter 6", report, samples)
|
| 287 |
-
save_run(args.out, model=model, config=vars(args), report=report, losses=losses, samples=samples)
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
if __name__ == "__main__":
|
| 291 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_6/meanflow.py
DELETED
|
@@ -1,175 +0,0 @@
|
|
| 1 |
-
"""MeanFlow, the Eulerian identity written in the average-velocity parameterization.
|
| 2 |
-
|
| 3 |
-
Substituting X_{s,t}(x) = x + (t-s) v_{s,t}(x) into the Eulerian identity leaves one equation,
|
| 4 |
-
v_{s,t}(x) = b(x,s) + (t-s) d/ds v_{s,t}(x), whose correction term is a total derivative along the
|
| 5 |
-
trajectory. One forward-mode pass along the tangent (b, 1, 0) in the arguments (x, s, t) computes
|
| 6 |
-
that derivative without forming a Jacobian, and the target uses the realized velocity x_1 - x_0 of
|
| 7 |
-
the interpolant in place of b, so a step costs one forward pass and one derivative.
|
| 8 |
-
|
| 9 |
-
Chapter 6, Sections 6.13, 6.15 and 6.18 of the notes. Trained on the four Gaussian blobs of
|
| 10 |
-
``cis6270.data.gaussian_mixture_2d``. Prints the identity worked by hand on dx/dt = x, where the
|
| 11 |
-
average velocity is 1.718x, the instantaneous velocity is x and the correction is 0.718x, and then
|
| 12 |
-
one-step, two-step and four-step samples from the trained map.
|
| 13 |
-
|
| 14 |
-
python lecture_6/meanflow.py # 2000 steps, about twenty seconds
|
| 15 |
-
python lecture_6/meanflow.py --steps 20 # a few seconds, for a first look
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
import math
|
| 20 |
-
|
| 21 |
-
import torch
|
| 22 |
-
|
| 23 |
-
from cis6270.data import gaussian_mixture_2d
|
| 24 |
-
from cis6270.metrics import mean_log_density, mode_report
|
| 25 |
-
from cis6270.nets import TwoTimeMLP, count_parameters
|
| 26 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed, train
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
torch.set_num_threads(1) # These networks are small, and splitting one matrix product costs more
|
| 30 |
-
# than it saves. ``tests/conftest.py`` sets the same thing for the test session, so the line
|
| 31 |
-
# here is the one that acts when a reader runs this script directly.
|
| 32 |
-
|
| 33 |
-
DIAGONAL_FRACTION = 0.25 # Share of each batch drawn with t = s, where the identity is flow matching.
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
# %% 02. The average velocity of the scalar flow, in closed form
|
| 37 |
-
#
|
| 38 |
-
# The exact map of dx/dt = x multiplies the state by e^{t-s}, so the displacement over the elapsed
|
| 39 |
-
# time is x (e^{t-s} - 1)/(t-s). On the diagonal the limit is x, which is b(x,t).
|
| 40 |
-
def scalar_average_velocity(x, s, t):
|
| 41 |
-
"""v_{s,t}(x) = x (e^{t-s} - 1)/(t - s) on the flow dx/dt = x."""
|
| 42 |
-
elapsed = t - s
|
| 43 |
-
return x * torch.expm1(elapsed) / elapsed # expm1 keeps the limit accurate near the diagonal.
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
# %% 03. The identity, checked on the scalar flow by hand
|
| 47 |
-
#
|
| 48 |
-
# v_{s,t}(x) = b(x,s) + (t-s) d/ds v_{s,t}(x), with d/ds = partial_s + b(x,s) . grad_x. At s = 0,
|
| 49 |
-
# t = 1 the partial derivative is -x, the spatial gradient is e - 1, and the transport term is
|
| 50 |
-
# therefore (e-1)x, so the correction is x(e-2) = 0.718x.
|
| 51 |
-
def scalar_check(x_value: float = 1.0):
|
| 52 |
-
"""Return every term of the identity at s = 0, t = 1, each one computed and not quoted."""
|
| 53 |
-
x = torch.tensor(x_value)
|
| 54 |
-
s, t = torch.tensor(0.0), torch.tensor(1.0)
|
| 55 |
-
average = scalar_average_velocity(x, s, t) # v_{0,1}(x) = x(e-1).
|
| 56 |
-
instant = x # b(x, 0) = x on this flow.
|
| 57 |
-
partial_s = torch.func.jvp( # Derivative in the start time alone, with x and t held fixed.
|
| 58 |
-
lambda ss: scalar_average_velocity(x, ss, t), (s,), (torch.ones_like(s),))[1]
|
| 59 |
-
gradient_x = torch.func.jvp( # Derivative in the state alone, the other half of d/ds.
|
| 60 |
-
lambda xx: scalar_average_velocity(xx, s, t), (x,), (torch.ones_like(x),))[1]
|
| 61 |
-
transport = instant * gradient_x # b(x,s) . grad_x v, the second piece of the total derivative.
|
| 62 |
-
total = partial_s + transport # d/ds v_{s,t}(x) = x(e - 2).
|
| 63 |
-
return {
|
| 64 |
-
"v_0,1(1)": float(average),
|
| 65 |
-
"b(1, 0)": float(instant),
|
| 66 |
-
"partial_s v": float(partial_s),
|
| 67 |
-
"grad_x v_0,1": float(gradient_x),
|
| 68 |
-
"d/ds v_0,1": float(total),
|
| 69 |
-
"identity gap": float((average - (instant + (t - s) * total)).abs()),
|
| 70 |
-
"one map evaluation": float(x + (t - s) * average),
|
| 71 |
-
"one Euler step": float(x + (t - s) * instant),
|
| 72 |
-
"e": math.e,
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
# %% 04. The Gaussian flow, where the correction vanishes
|
| 77 |
-
#
|
| 78 |
-
# A linear interpolant between paired Gaussians produces straight trajectories, and on a straight
|
| 79 |
-
# trajectory the average velocity equals the initial velocity, so the total derivative is zero.
|
| 80 |
-
def gaussian_check(x_value: float = 0.7):
|
| 81 |
-
"""v_{0,t}(x) = 2 + 2x for every end time, and b(x,0) is the same number."""
|
| 82 |
-
x = torch.tensor(x_value)
|
| 83 |
-
average = torch.stack([ # Three end times, all giving the same average velocity.
|
| 84 |
-
(2.0 * t + (1.0 + 2.0 * t) * x - x) / t for t in (0.25, 0.5, 1.0)])
|
| 85 |
-
instant = 2.0 + 2.0 * (x - 0.0) / 1.0 # b(x, 0) on the Gaussian flow.
|
| 86 |
-
return {"v_0,t(0.7) spread": float(average.max() - average.min()),
|
| 87 |
-
"v_0,1(0.7)": float(average[-1]), "b(0.7, 0)": float(instant)}
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
# %% 05. The MeanFlow target, by one forward-mode pass
|
| 91 |
-
#
|
| 92 |
-
# The tangent (x_1 - x_0, 1, 0) in the arguments (x, s, t) contracts the full Jacobian of v into
|
| 93 |
-
# the total derivative along the trajectory: the realized velocity in the state slot, one in the
|
| 94 |
-
# start-time slot, and zero in the end-time slot because the end time is held fixed.
|
| 95 |
-
def mean_flow_target(model, xs, s, t, realized):
|
| 96 |
-
"""b(x,s) + (t-s) d/ds v_{s,t}(x), detached, which is the right-hand side of the identity."""
|
| 97 |
-
f = lambda xx, ss: model(xx, ss, t)
|
| 98 |
-
_, total = torch.func.jvp(f, (xs, s), (realized, torch.ones_like(s))) # [B, d] d/ds v.
|
| 99 |
-
return (realized + (t - s)[:, None] * total).detach() # [B, d] the target, held fixed.
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def make_loss(model, data, batch_size, device):
|
| 103 |
-
"""Draw a coupling, draw times from the triangle, and regress v onto its own identity."""
|
| 104 |
-
|
| 105 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 106 |
-
index = torch.randint(0, len(data), (batch_size,), device=device)
|
| 107 |
-
x1 = data[index] # [B, 2] the data draw.
|
| 108 |
-
x0 = torch.randn_like(x1) # [B, 2] the source draw.
|
| 109 |
-
s = torch.rand(batch_size, device=device) # [B] start time.
|
| 110 |
-
t = s + (1.0 - s) * torch.rand(batch_size, device=device) # [B] end time, s <= t <= 1.
|
| 111 |
-
diagonal = torch.rand(batch_size, device=device) < DIAGONAL_FRACTION # [B] which rows anchor.
|
| 112 |
-
t = torch.where(diagonal, s, t) # On those rows the correction term has a factor of zero.
|
| 113 |
-
xs = (1 - s[:, None]) * x0 + s[:, None] * x1 # [B, 2] the interpolant I_s.
|
| 114 |
-
realized = x1 - x0 # [B, 2] the derivative of the interpolant, available in closed form.
|
| 115 |
-
target = mean_flow_target(model, xs, s, t, realized)
|
| 116 |
-
return ((model(xs, s, t) - target) ** 2).mean()
|
| 117 |
-
|
| 118 |
-
return loss_fn
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
# %% 06. Sampling, at any step count the reader chooses
|
| 122 |
-
@torch.no_grad()
|
| 123 |
-
def sample(model, x0, steps: int):
|
| 124 |
-
"""Cross [0, 1] in equal pieces, one evaluation of the map per piece."""
|
| 125 |
-
x = x0
|
| 126 |
-
grid = torch.linspace(0.0, 1.0, steps + 1, device=x0.device) # [steps+1] the time grid.
|
| 127 |
-
for s, t in zip(grid[:-1], grid[1:]):
|
| 128 |
-
x = model.flow_map(x, s.expand(len(x)), t.expand(len(x))) # X_{s,t}(x).
|
| 129 |
-
return x # [N, 2] generated points.
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
# %% 07. The report
|
| 133 |
-
def main() -> None:
|
| 134 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 135 |
-
parser.set_defaults(steps=2000, batch_size=128, samples=512)
|
| 136 |
-
args = parser.parse_args()
|
| 137 |
-
set_seed(args.seed)
|
| 138 |
-
device = pick_device(args.device)
|
| 139 |
-
data = gaussian_mixture_2d(num_points=2048, seed=args.seed).to(device) # [2048, 2].
|
| 140 |
-
|
| 141 |
-
report = scalar_check()
|
| 142 |
-
report.update(gaussian_check())
|
| 143 |
-
if not args.quiet:
|
| 144 |
-
print(f"on dx/dt = x at s=0, t=1: v = {report['v_0,1(1)']:.6f} = b {report['b(1, 0)']:.6f}"
|
| 145 |
-
f" + correction {report['d/ds v_0,1']:.6f}")
|
| 146 |
-
print(f"one map evaluation {report['one map evaluation']:.6f} against one Euler step "
|
| 147 |
-
f"{report['one Euler step']:.6f}, exact e = {math.e:.6f}")
|
| 148 |
-
|
| 149 |
-
model = TwoTimeMLP(dim=2, width=args.width).to(device)
|
| 150 |
-
if not args.quiet:
|
| 151 |
-
print(f"\ntraining MeanFlow parameters={count_parameters(model):,}")
|
| 152 |
-
losses = train(model, make_loss(model, data, args.batch_size, device),
|
| 153 |
-
steps=args.steps, lr=args.lr, quiet=args.quiet)
|
| 154 |
-
|
| 155 |
-
torch.manual_seed(args.seed + 1)
|
| 156 |
-
x0 = torch.randn(args.samples, 2, device=device) # [N, 2] the source points every run shares.
|
| 157 |
-
samples: list[str] = []
|
| 158 |
-
for steps in (1, 2, 4):
|
| 159 |
-
points = sample(model, x0, steps)
|
| 160 |
-
stats = mode_report(points)
|
| 161 |
-
report[f"{steps}-step nearest distance"] = stats["nearest_distance"]
|
| 162 |
-
report[f"{steps}-step mode entropy"] = stats["mode_entropy"]
|
| 163 |
-
report[f"{steps}-step log density"] = mean_log_density(points.cpu())
|
| 164 |
-
samples.append(f"{steps} step(s): nearest {stats['nearest_distance']:.3f} "
|
| 165 |
-
f"modes {stats['modes_found']} entropy {stats['mode_entropy']:.3f}")
|
| 166 |
-
one, four = sample(model, x0, 1), sample(model, x0, 4)
|
| 167 |
-
report["one step against four"] = float((one - four).norm(dim=1).mean())
|
| 168 |
-
report["final loss"] = sum(losses[-20:]) / len(losses[-20:])
|
| 169 |
-
|
| 170 |
-
print_report("MeanFlow, Chapter 6", report, samples)
|
| 171 |
-
save_run(args.out, model=model, config=vars(args), report=report, losses=losses, samples=samples)
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
if __name__ == "__main__":
|
| 175 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_6/posterior_map.py
DELETED
|
@@ -1,221 +0,0 @@
|
|
| 1 |
-
"""Diamond Maps and Meta Flow Maps, the posterior over endpoints that guidance needs.
|
| 2 |
-
|
| 3 |
-
Steering a flow toward a reward needs the value function, the log expected exponential reward over
|
| 4 |
-
the terminal states reachable from the current one, and that average runs over a posterior. A
|
| 5 |
-
deterministic map returns one endpoint, so the only estimate it supports is the reward at that
|
| 6 |
-
endpoint, which is low by the Jensen gap. Giving the map a fresh noise argument and indexing it by
|
| 7 |
-
the state it is conditioned on turns it into a family of inner flow maps, one per conditioning
|
| 8 |
-
pair, whose draws are posterior samples.
|
| 9 |
-
|
| 10 |
-
Chapter 6, Sections 6.33 and 6.34 of the notes. Runs on a two-point reward problem and on the four
|
| 11 |
-
Gaussian blobs of ``cis6270.data.gaussian_mixture_2d``. Prints the Jensen gap of 0.434, the bias
|
| 12 |
-
of the K-sample value at K = 1, 2, 4 and 8, the softmax-weighted value gradient of -0.4483, the
|
| 13 |
-
importance weight that pulls a renoised proposal from 0.420 back to 0.0131, and the stop-gradient
|
| 14 |
-
fixed point 0.625 of Meta Flow Maps.
|
| 15 |
-
|
| 16 |
-
python lecture_6/posterior_map.py # 16 inner samples per state
|
| 17 |
-
python lecture_6/posterior_map.py --steps 20 # fewer inner substeps, for a first look
|
| 18 |
-
"""
|
| 19 |
-
|
| 20 |
-
# %% 01. Imports and configuration
|
| 21 |
-
import math
|
| 22 |
-
|
| 23 |
-
import torch
|
| 24 |
-
|
| 25 |
-
from cis6270.data import gaussian_mixture_2d
|
| 26 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed
|
| 27 |
-
|
| 28 |
-
torch.set_num_threads(1) # The work here is small, and one thread keeps the timing stable.
|
| 29 |
-
# ``tests/conftest.py`` sets the same thing for the test session, so the line here is the one
|
| 30 |
-
# that acts when a reader runs this script directly.
|
| 31 |
-
torch.set_default_dtype(torch.float64) # Exact posteriors, compared against sample averages.
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
INNER_STEPS = 48 # Substeps of the inner flow, which carries noise to a posterior sample.
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
# %% 02. The tilt, the value function and the Jensen gap
|
| 38 |
-
#
|
| 39 |
-
# The tilted target is the data times the exponential reward, and the same reweighting acts on the
|
| 40 |
-
# posterior over endpoints at every intermediate state. A deterministic map returns the posterior
|
| 41 |
-
# mean, so its estimate of the value is the reward there, and the exponential is convex, so that
|
| 42 |
-
# estimate is always the low one.
|
| 43 |
-
def two_point(reward_gap: float = 2.0, x: float = 0.7, t: float = 0.8):
|
| 44 |
-
"""The chapter's two-point example, with z in {0, 2}, r(z) = z and a linear schedule."""
|
| 45 |
-
weights = torch.tensor([1.0, math.exp(reward_gap)]) # e^{r(0)} and e^{r(2)}.
|
| 46 |
-
tilted = weights / weights.sum() # The tilted target over the two points.
|
| 47 |
-
far = math.exp(-((x - t * 2.0) ** 2) / (2.0 * (1.0 - t) ** 2)) # Unnormalized, at z = 2.
|
| 48 |
-
near = math.exp(-(x ** 2) / (2.0 * (1.0 - t) ** 2)) # Unnormalized, at z = 0.
|
| 49 |
-
posterior = torch.tensor([near, far]) / (near + far) # The posterior over endpoints at x.
|
| 50 |
-
value = math.log(float((posterior * weights).sum())) # V_t(x) at that state.
|
| 51 |
-
tilted_posterior = posterior * weights / float((posterior * weights).sum())
|
| 52 |
-
exact = math.log(0.5 + 0.5 * math.exp(reward_gap)) # V over the data itself.
|
| 53 |
-
return {
|
| 54 |
-
"tilted target": [round(v, 4) for v in tilted.tolist()],
|
| 55 |
-
"posterior at x=0.7": [round(v, 4) for v in posterior.tolist()],
|
| 56 |
-
"tilted posterior": [round(v, 4) for v in tilted_posterior.tolist()],
|
| 57 |
-
"V_t(0.7)": value,
|
| 58 |
-
"V over the data": exact,
|
| 59 |
-
"single-endpoint estimate": 1.0 * reward_gap / 2.0, # r at the posterior mean z = 1.
|
| 60 |
-
"Jensen gap": exact - reward_gap / 2.0,
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def value_bias(reward_gap: float = 2.0):
|
| 65 |
-
"""E[V_K] over K independent posterior draws, which approaches the exact value like 1/K."""
|
| 66 |
-
exact = math.log(0.5 + 0.5 * math.exp(reward_gap))
|
| 67 |
-
out = {}
|
| 68 |
-
for k in (1, 2, 4, 8):
|
| 69 |
-
expectation = sum(
|
| 70 |
-
math.comb(k, j) * 2.0 ** (-k)
|
| 71 |
-
* math.log(((k - j) + j * math.exp(reward_gap)) / k) for j in range(k + 1))
|
| 72 |
-
out[f"E[V_{k}]"] = expectation
|
| 73 |
-
out[f"bias at K={k}"] = exact - expectation
|
| 74 |
-
return out
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
# %% 03. The value gradient from K samples
|
| 78 |
-
#
|
| 79 |
-
# Differentiating the logarithm of an average of exponentials produces a softmax over the rewards,
|
| 80 |
-
# so a sample whose reward is far above the others dominates the direction and a set with equal
|
| 81 |
-
# rewards gives their plain average.
|
| 82 |
-
def value_gradient(rewards, gradients):
|
| 83 |
-
"""The softmax weights over the rewards, and the weighted average of the reward gradients."""
|
| 84 |
-
weights = torch.softmax(rewards, dim=0) # [K].
|
| 85 |
-
return weights, float((weights * gradients).sum()), float(gradients.mean())
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
# %% 04. The inner clock
|
| 89 |
-
#
|
| 90 |
-
# For a fixed conditioning pair the map is an ordinary two-time flow map on an inner time axis
|
| 91 |
-
# running from the noise draw at 0 to a posterior sample at 1. Its velocity is the flow matching
|
| 92 |
-
# velocity of the posterior, which is available in closed form over the training points.
|
| 93 |
-
def posterior_weights(x, t, data):
|
| 94 |
-
"""p_{1|t}(z_i | x), a softmax over the training points under the linear interpolant. [B, N]."""
|
| 95 |
-
spread = max(1.0 - t, 1e-6)
|
| 96 |
-
squared = ((x[:, None, :] - t * data[None]) ** 2).sum(-1) # [B, N].
|
| 97 |
-
return torch.softmax(-squared / (2.0 * spread ** 2), dim=1) # [B, N].
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def inner_map(noise, weights, data, substeps: int):
|
| 101 |
-
"""Y_{0,1}(eps | x, t), the inner flow from a Gaussian draw to a posterior sample. [B, K, 2]."""
|
| 102 |
-
y = noise # [B, K, 2] one inner state per sample.
|
| 103 |
-
step = 1.0 / substeps
|
| 104 |
-
for i in range(substeps):
|
| 105 |
-
a = i * step # The inner time.
|
| 106 |
-
squared = ((y[:, :, None, :] - a * data[None, None]) ** 2).sum(-1) # [B, K, N].
|
| 107 |
-
inner = torch.softmax(torch.log(weights[:, None, :].clamp_min(1e-30))
|
| 108 |
-
- squared / (2.0 * max(1.0 - a, 1e-6) ** 2), dim=-1) # [B, K, N].
|
| 109 |
-
denoiser = inner @ data # [B, K, 2] the inner posterior mean.
|
| 110 |
-
y = y + step * (denoiser - y) / max(1.0 - a, 1e-6) # Euler on the inner velocity.
|
| 111 |
-
return y
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
# %% 05. Posterior samples from a map we already have
|
| 115 |
-
#
|
| 116 |
-
# We can renoise the current state back to an earlier time with fresh noise and push it through
|
| 117 |
-
# the existing map, which discards information and widens the law of the endpoint. The correction
|
| 118 |
-
# is an importance weight, written as a shift of the reward by the log likelihood of the current
|
| 119 |
-
# state given that endpoint.
|
| 120 |
-
def renoise_example(x: float = 0.7, t: float = 0.8, earlier: float = 0.6):
|
| 121 |
-
"""The chapter's renoised proposal, and the importance weight that recovers the posterior."""
|
| 122 |
-
scale = earlier / t # alpha_{t'}/alpha_t on the linear schedule.
|
| 123 |
-
variance = (1.0 - earlier) ** 2 - scale ** 2 * (1.0 - t) ** 2 # The variance added back.
|
| 124 |
-
mean = scale * x # The center of the proposal.
|
| 125 |
-
boundary = (earlier * 2.0 / 2.0 - mean) / math.sqrt(variance) # Where the map switches mode.
|
| 126 |
-
proposal_far = 0.5 * math.erfc(boundary / math.sqrt(2.0)) # Share of draws reaching z = 2.
|
| 127 |
-
penalty_far = (x - t * 2.0) ** 2 / (2.0 * (1.0 - t) ** 2) # Log likelihood of x given z = 2.
|
| 128 |
-
penalty_near = x ** 2 / (2.0 * (1.0 - t) ** 2) # The same for z = 0.
|
| 129 |
-
far = proposal_far * math.exp(-penalty_far)
|
| 130 |
-
near = (1.0 - proposal_far) * math.exp(-penalty_near)
|
| 131 |
-
return {"renoise scale": scale, "renoise variance": variance,
|
| 132 |
-
"renoise standard deviation": math.sqrt(variance),
|
| 133 |
-
"proposal weight on the far mode": proposal_far,
|
| 134 |
-
"penalty at z=2": penalty_far, "penalty at z=0": penalty_near,
|
| 135 |
-
"corrected weight on the far mode": far / (far + near)}
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
# %% 06. Meta Flow Maps, the stop-gradient form
|
| 139 |
-
#
|
| 140 |
-
# The controlled velocity needs a ratio of two expectations, and regressing onto a per-sample
|
| 141 |
-
# ratio is biased at every sample size. Multiplying through by the denominator leaves a condition
|
| 142 |
-
# with one stop-gradient whose fixed point is the ratio of the expectations.
|
| 143 |
-
def stop_gradient_fixed_point(weights, gradients, diffusion_squared: float = 2.0):
|
| 144 |
-
"""The fixed point of the loss, the per-sample ratio it is compared against, and the gap."""
|
| 145 |
-
exact = 0.5 * diffusion_squared * float(gradients.mean()) / float(weights.mean())
|
| 146 |
-
naive = float((gradients / weights).mean()) * 0.5 * diffusion_squared
|
| 147 |
-
covariance = float(gradients.mean()) - float((gradients / weights).mean() * weights.mean())
|
| 148 |
-
return {"E[w]": float(weights.mean()), "E[G]": float(gradients.mean()),
|
| 149 |
-
"stop-gradient fixed point": exact, "per-sample ratio average": naive,
|
| 150 |
-
"gap": naive - exact, "covariance of G/w and w": covariance,
|
| 151 |
-
"gap from the covariance": -covariance / float(weights.mean())}
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
# %% 07. The report
|
| 155 |
-
def main() -> None:
|
| 156 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 157 |
-
parser.set_defaults(steps=INNER_STEPS, samples=16)
|
| 158 |
-
args = parser.parse_args() # --steps is the inner substep count here, since nothing trains.
|
| 159 |
-
set_seed(args.seed)
|
| 160 |
-
pick_device(args.device) # Every number here is double precision, which not every
|
| 161 |
-
# accelerator carries, so the work stays on the CPU whatever --device names.
|
| 162 |
-
substeps = max(args.steps, 8)
|
| 163 |
-
|
| 164 |
-
report = two_point()
|
| 165 |
-
report.update(value_bias())
|
| 166 |
-
weights, weighted, plain = value_gradient(torch.tensor([0.2, 1.5, 0.9]),
|
| 167 |
-
torch.tensor([0.4, -1.2, 0.5]))
|
| 168 |
-
report["softmax weights"] = [round(v, 4) for v in weights.tolist()]
|
| 169 |
-
report["weighted value gradient"] = weighted
|
| 170 |
-
report["plain average gradient"] = plain
|
| 171 |
-
report.update(renoise_example())
|
| 172 |
-
report.update(stop_gradient_fixed_point(torch.tensor([1.0, 2.0, 5.0]),
|
| 173 |
-
torch.tensor([0.4, 1.0, 3.6])))
|
| 174 |
-
if not args.quiet:
|
| 175 |
-
print(f"Jensen gap {report['Jensen gap']:.3f} between the exact value "
|
| 176 |
-
f"{report['V over the data']:.4f} and the single endpoint "
|
| 177 |
-
f"{report['single-endpoint estimate']:.3f}")
|
| 178 |
-
print(f"renoised proposal {report['proposal weight on the far mode']:.3f} corrected to "
|
| 179 |
-
f"{report['corrected weight on the far mode']:.4f} against the posterior "
|
| 180 |
-
f"{report['posterior at x=0.7'][1]:.4f}")
|
| 181 |
-
|
| 182 |
-
# %% 08. The same quantities on the four Gaussian blobs
|
| 183 |
-
data = gaussian_mixture_2d(num_points=256, seed=args.seed).double() # [N, 2] the endpoints.
|
| 184 |
-
outer_t = 0.6 # The outer time the sampler is conditioned at.
|
| 185 |
-
torch.manual_seed(args.seed + 1)
|
| 186 |
-
index = torch.randint(0, len(data), (32,))
|
| 187 |
-
states = ((1.0 - outer_t) * torch.randn(32, 2) + outer_t * data[index]) # [32, 2] contexts.
|
| 188 |
-
states.requires_grad_(True)
|
| 189 |
-
exact_weights = posterior_weights(states, outer_t, data) # [32, N] the exact posterior.
|
| 190 |
-
reward = data[:, 0] # r(z) = the first coordinate, a reward with a clear direction.
|
| 191 |
-
exact_value = torch.log((exact_weights * reward.exp()[None, :]).sum(1)) # [32] V_t(x).
|
| 192 |
-
exact_grad = torch.autograd.grad(exact_value.sum(), states, retain_graph=True)[0] # [32, 2].
|
| 193 |
-
|
| 194 |
-
noise = torch.randn(32, args.samples, 2) # [32, K, 2] inner draws, one per posterior sample.
|
| 195 |
-
drawn = inner_map(noise, exact_weights, data, substeps) # [32, K, 2] posterior samples.
|
| 196 |
-
sampled_reward = drawn[:, :, 0] # [32, K] the reward at each sample.
|
| 197 |
-
estimate = torch.logsumexp(sampled_reward, dim=1) - math.log(args.samples) # [32] V hat.
|
| 198 |
-
sample_grad = torch.autograd.grad(estimate.sum(), states, retain_graph=True)[0] # [32, 2].
|
| 199 |
-
deterministic = (exact_weights @ reward) # [32] the reward at the posterior mean.
|
| 200 |
-
|
| 201 |
-
exact_value, estimate = exact_value.detach(), estimate.detach() # Past the two gradients.
|
| 202 |
-
deterministic, drawn = deterministic.detach(), drawn.detach()
|
| 203 |
-
report["exact V, averaged"] = float(exact_value.mean())
|
| 204 |
-
report[f"V from {args.samples} samples"] = float(estimate.mean())
|
| 205 |
-
report["single endpoint, averaged"] = float(deterministic.mean())
|
| 206 |
-
report["Jensen gap on the blobs"] = float((exact_value - deterministic).mean())
|
| 207 |
-
report["posterior mean error"] = float(
|
| 208 |
-
(drawn.mean(1) - exact_weights.detach() @ data).norm(dim=1).mean())
|
| 209 |
-
report["value gradient error"] = float((sample_grad - exact_grad).norm(dim=1).mean())
|
| 210 |
-
report["exact value gradient norm"] = float(exact_grad.norm(dim=1).mean())
|
| 211 |
-
states = states.detach()
|
| 212 |
-
samples = [f"state ({float(x[0]):+.2f}, {float(x[1]):+.2f}) exact V {float(v):.3f} "
|
| 213 |
-
f"{args.samples} samples {float(e):.3f} one endpoint {float(d):.3f}"
|
| 214 |
-
for x, v, e, d in zip(states[:4], exact_value[:4], estimate[:4], deterministic[:4])]
|
| 215 |
-
|
| 216 |
-
print_report("Diamond Maps and Meta Flow Maps, Chapter 6", report, samples)
|
| 217 |
-
save_run(args.out, config=vars(args), report=report, samples=samples)
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
if __name__ == "__main__":
|
| 221 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_6/residuals.py
DELETED
|
@@ -1,204 +0,0 @@
|
|
| 1 |
-
"""The three residuals of a flow map, and the diagonal anchor added to each one.
|
| 2 |
-
|
| 3 |
-
The cocycle differentiated in the end time is the Lagrangian identity, differentiated in the start
|
| 4 |
-
time it is the Eulerian identity, and left finite it is the composition identity. Each becomes a
|
| 5 |
-
loss by squaring the amount the model violates it. None of the three mentions the data, so each is
|
| 6 |
-
satisfied by a model that is self-consistent and wrong, and a flow matching loss on the diagonal
|
| 7 |
-
supplies the one piece of information about the target.
|
| 8 |
-
|
| 9 |
-
Chapter 6, Sections 6.15 to 6.17 and 6.23 of the notes. Trained on the four Gaussian blobs
|
| 10 |
-
of ``cis6270.data.gaussian_mixture_2d``. Prints the stay-still residuals of Section 6.17, which are
|
| 11 |
-
1, 1 and 0, and then four trained models side by side in one-step sampling.
|
| 12 |
-
|
| 13 |
-
python lecture_6/residuals.py # four models, 600 steps each
|
| 14 |
-
python lecture_6/residuals.py --steps 20 # a few seconds, for a first look
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
# %% 01. Imports and configuration
|
| 18 |
-
import torch
|
| 19 |
-
|
| 20 |
-
from cis6270.data import GAUSSIAN_STD, gaussian_mixture_2d
|
| 21 |
-
from cis6270.metrics import mode_report
|
| 22 |
-
from cis6270.nets import TwoTimeMLP, count_parameters
|
| 23 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed, train
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
torch.set_num_threads(1) # These networks are small, and splitting one matrix product costs more
|
| 27 |
-
# than it saves. ``tests/conftest.py`` sets the same thing for the test session, so the line
|
| 28 |
-
# here is the one that acts when a reader runs this script directly.
|
| 29 |
-
|
| 30 |
-
ON_MODE = 3.0 * GAUSSIAN_STD # A sample counts as on a mode within three data standard deviations.
|
| 31 |
-
ODE_STEPS = 128 # Substeps used to integrate the model's own diagonal velocity accurately.
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
# %% 02. The model, whose identity axiom is free
|
| 35 |
-
#
|
| 36 |
-
# The network produces the average velocity v_{s,t}(x), and the map is the state plus the elapsed
|
| 37 |
-
# time times that velocity. At t = s the second term vanishes whatever the weights are, so no term
|
| 38 |
-
# of the loss is spent on the identity axiom.
|
| 39 |
-
class Map(torch.nn.Module):
|
| 40 |
-
"""X_{s,t}(x) = x + (t - s) v_{s,t}(x), with v the two-time network of ``cis6270.nets``."""
|
| 41 |
-
|
| 42 |
-
def __init__(self, dim: int = 2, width: int = 128):
|
| 43 |
-
super().__init__()
|
| 44 |
-
self.net = TwoTimeMLP(dim=dim, width=width) # Reads x, s and t; returns a vector.
|
| 45 |
-
|
| 46 |
-
def v(self, x, s, t): # the network
|
| 47 |
-
return self.net(x, s, t) # [B, d] the average velocity over [s, t].
|
| 48 |
-
|
| 49 |
-
def X(self, x, s, t): # identity axiom, free
|
| 50 |
-
return self.net.flow_map(x, s, t) # [B, d] = x + (t - s) v_{s,t}(x).
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
# %% 03. The diagonal anchor, which is the tangent axiom
|
| 54 |
-
#
|
| 55 |
-
# I_t = (1-t) x_0 + t x_1 and its derivative x_1 - x_0 are available in closed form, so the anchor
|
| 56 |
-
# costs one network call and no simulation. Its minimizer is E[I_dot | I_t], the true velocity.
|
| 57 |
-
def anchor(m, x0, x1, t): # tangent axiom
|
| 58 |
-
xt = (1 - t[:, None]) * x0 + t[:, None] * x1 # [B, d] the interpolant at time t.
|
| 59 |
-
return ((m.v(xt, t, t) - (x1 - x0)) ** 2).mean()
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
# %% 04. The Lagrangian residual
|
| 63 |
-
#
|
| 64 |
-
# d/dt X = b at the output. The closure holds the state and the start time fixed, and a tangent of
|
| 65 |
-
# one in t returns the map and its end-time derivative from a single forward-mode pass. The target
|
| 66 |
-
# is the model's own diagonal velocity, detached, so the diagonal trains only through the anchor.
|
| 67 |
-
def lagrangian(m, xs, s, t): # d/dt X = b at the output
|
| 68 |
-
f = lambda tt: m.X(xs, s, tt)
|
| 69 |
-
X, dXdt = torch.func.jvp(f, (t,), (torch.ones_like(t),))
|
| 70 |
-
return ((dXdt - m.v(X, t, t).detach()) ** 2).mean()
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
# %% 05. The Eulerian residual
|
| 74 |
-
#
|
| 75 |
-
# d/ds X + b . grad X = 0. Both the state and the start time are open in the closure, and the
|
| 76 |
-
# tangent (v, 1) makes the one returned directional derivative the whole left-hand side. The
|
| 77 |
-
# identity is already written as zero, so the residual needs no target at all.
|
| 78 |
-
def eulerian(m, xs, s, t): # d/ds X + b . grad X = 0
|
| 79 |
-
vs = m.v(xs, s, s).detach()
|
| 80 |
-
f = lambda xx, ss: m.X(xx, ss, t)
|
| 81 |
-
_, dX = torch.func.jvp(f, (xs, s), (vs, torch.ones_like(s)))
|
| 82 |
-
return (dX ** 2).mean()
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
# %% 06. The composition residual
|
| 86 |
-
#
|
| 87 |
-
# Two short maps against one long one, with no derivative of any kind. The detach covers the whole
|
| 88 |
-
# composed target, so the two legs are frozen and only the long map is corrected.
|
| 89 |
-
def composition(m, xs, s, u, t): # two short maps, one long one
|
| 90 |
-
tgt = m.X(m.X(xs, s, u), u, t).detach()
|
| 91 |
-
return ((m.X(xs, s, t) - tgt) ** 2).mean()
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
# %% 07. The stay-still solution each residual has to rule out
|
| 95 |
-
#
|
| 96 |
-
# The average velocity equals the true b when the two times coincide and zero everywhere else, so
|
| 97 |
-
# the map is the identity for every pair of times and the sampler generates the source. The
|
| 98 |
-
# identity axiom, the cocycle and the anchor all hold on it, and the two differential residuals
|
| 99 |
-
# separate it from the truth by a full unit.
|
| 100 |
-
class StayStill(torch.nn.Module):
|
| 101 |
-
"""v_{s,t}(x) = b(x,t) on the diagonal and 0 off it, on the scalar flow dx/dt = x."""
|
| 102 |
-
|
| 103 |
-
def v(self, x, s, t):
|
| 104 |
-
on_diagonal = (s == t)[:, None] # [B, 1] true where the two times meet.
|
| 105 |
-
return torch.where(on_diagonal, x, torch.zeros_like(x)) # b(x,t) = x on this flow.
|
| 106 |
-
|
| 107 |
-
def X(self, x, s, t):
|
| 108 |
-
return x # Off the diagonal (t-s) times zero, on it zero times b, so the map is the identity.
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def stay_still_residuals():
|
| 112 |
-
"""The three residuals of Section 6.17 at x = 1, s = 0, t = 1, which are 1, 1 and 0."""
|
| 113 |
-
m = StayStill()
|
| 114 |
-
x = torch.ones(1, 1) # [1, 1] the state x = 1.
|
| 115 |
-
s, t, u = torch.zeros(1), torch.ones(1), torch.full((1,), 0.5)
|
| 116 |
-
diagonal_gap = float(((m.v(x, t, t) - x) ** 2).mean()) # The anchor, at its minimum.
|
| 117 |
-
return {
|
| 118 |
-
"stay-still Lagrangian": float(lagrangian(m, x, s, t)),
|
| 119 |
-
"stay-still Eulerian": float(eulerian(m, x, s, t)),
|
| 120 |
-
"stay-still composition": float(composition(m, x, s, u, t)),
|
| 121 |
-
"stay-still diagonal gap": diagonal_gap,
|
| 122 |
-
}
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
# %% 08. One training step, the same shape whichever residual we add
|
| 126 |
-
def make_loss(model, data, residual, batch_size, device):
|
| 127 |
-
"""Draw a coupling, draw times from the triangle s <= t, and add the residual to the anchor."""
|
| 128 |
-
|
| 129 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 130 |
-
index = torch.randint(0, len(data), (batch_size,), device=device)
|
| 131 |
-
x1 = data[index] # [B, 2] the data draw.
|
| 132 |
-
x0 = torch.randn_like(x1) # [B, 2] the independent source draw.
|
| 133 |
-
s = torch.rand(batch_size, device=device) # [B] start time.
|
| 134 |
-
t = s + (1.0 - s) * torch.rand(batch_size, device=device) # [B] end time, s <= t <= 1.
|
| 135 |
-
u = s + (t - s) * torch.rand(batch_size, device=device) # [B] intermediate time.
|
| 136 |
-
total = anchor(model, x0, x1, torch.rand(batch_size, device=device)) # The data term.
|
| 137 |
-
if residual is None:
|
| 138 |
-
return total # The diagonal alone, which is flow matching read in one step.
|
| 139 |
-
xs = (1 - s[:, None]) * x0 + s[:, None] * x1 # [B, 2] the interpolant at the start time.
|
| 140 |
-
if residual is composition:
|
| 141 |
-
return total + residual(model, xs, s, u, t)
|
| 142 |
-
return total + residual(model, xs, s, t)
|
| 143 |
-
|
| 144 |
-
return loss_fn
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
# %% 09. One-step sampling, and the model's own ordinary differential equation
|
| 148 |
-
@torch.no_grad()
|
| 149 |
-
def evaluate(model, x0):
|
| 150 |
-
"""One-step output, the endpoint of the model's own diagonal velocity, and the mode statistics."""
|
| 151 |
-
from cis6270.data import GAUSSIAN_CENTERS
|
| 152 |
-
|
| 153 |
-
zeros = torch.zeros(len(x0), device=x0.device)
|
| 154 |
-
one_step = model.X(x0, zeros, torch.ones_like(zeros)) # [N, 2] a single evaluation of the map.
|
| 155 |
-
x, h = x0.clone(), 1.0 / ODE_STEPS # Integrate dx/dt = v_{t,t}(x) with the same weights.
|
| 156 |
-
for k in range(ODE_STEPS):
|
| 157 |
-
t = torch.full((len(x0),), k * h, device=x0.device)
|
| 158 |
-
x = x + h * model.v(x, t, t) # Euler on the diagonal velocity the anchor trains.
|
| 159 |
-
distance = float((one_step - x).norm(dim=1).mean()) # Column two of Table 6.2.
|
| 160 |
-
nearest = torch.cdist(one_step, GAUSSIAN_CENTERS.to(one_step)).min(dim=1).values # [N].
|
| 161 |
-
on_mode = float((nearest < ON_MODE).to(torch.get_default_dtype()).mean()) # Column three of Table 6.2.
|
| 162 |
-
return one_step, distance, on_mode, mode_report(one_step)
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
# %% 10. The report
|
| 166 |
-
def main() -> None:
|
| 167 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 168 |
-
parser.set_defaults(samples=512)
|
| 169 |
-
args = parser.parse_args()
|
| 170 |
-
set_seed(args.seed)
|
| 171 |
-
device = pick_device(args.device)
|
| 172 |
-
data = gaussian_mixture_2d(num_points=2048, seed=args.seed).to(device) # [2048, 2] the target.
|
| 173 |
-
|
| 174 |
-
report = stay_still_residuals() # Section 6.17, before any training.
|
| 175 |
-
if not args.quiet:
|
| 176 |
-
print(f"stay-still at x=1, s=0, t=1: Lagrangian {report['stay-still Lagrangian']:.3f} "
|
| 177 |
-
f"Eulerian {report['stay-still Eulerian']:.3f} "
|
| 178 |
-
f"composition {report['stay-still composition']:.3f}")
|
| 179 |
-
|
| 180 |
-
recipes = [("the diagonal alone", None), ("+ Lagrangian", lagrangian),
|
| 181 |
-
("+ Eulerian", eulerian), ("+ composition", composition)]
|
| 182 |
-
samples: list[str] = []
|
| 183 |
-
for name, residual in recipes:
|
| 184 |
-
set_seed(args.seed) # Same starting weights, same batches and same noise for all four.
|
| 185 |
-
model = Map(dim=2, width=args.width).to(device)
|
| 186 |
-
if not args.quiet:
|
| 187 |
-
print(f"\ntraining {name} parameters={count_parameters(model):,}")
|
| 188 |
-
losses = train(model, make_loss(model, data, residual, args.batch_size, device),
|
| 189 |
-
steps=args.steps, lr=args.lr, quiet=args.quiet)
|
| 190 |
-
torch.manual_seed(args.seed + 1) # The same source points for every model.
|
| 191 |
-
x0 = torch.randn(args.samples, 2, device=device)
|
| 192 |
-
_, distance, on_mode, stats = evaluate(model, x0)
|
| 193 |
-
report[f"{name}: distance to its own ODE"] = distance
|
| 194 |
-
report[f"{name}: fraction on a mode"] = on_mode
|
| 195 |
-
report[f"{name}: final loss"] = sum(losses[-20:]) / len(losses[-20:])
|
| 196 |
-
samples.append(f"{name:<20s} distance {distance:.3f} on a mode {on_mode:.2f} "
|
| 197 |
-
f"modes found {stats['modes_found']}")
|
| 198 |
-
|
| 199 |
-
print_report("Three residuals and the diagonal anchor, Chapter 6", report, samples)
|
| 200 |
-
save_run(args.out, config=vars(args), report=report, samples=samples)
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
if __name__ == "__main__":
|
| 204 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_6/shortcut.py
DELETED
|
@@ -1,184 +0,0 @@
|
|
| 1 |
-
"""Shortcut models, the composition identity indexed by step size.
|
| 2 |
-
|
| 3 |
-
The second input of the network is the length of the step and not the time at which it ends, and
|
| 4 |
-
the identity is enforced only between a step and the step twice as long. One doubling constraint
|
| 5 |
-
suffices because the ladder chains: with a finest step of 1/128 and seven doublings, the one-step
|
| 6 |
-
map at level seven rests on seven relations and one regression against the data.
|
| 7 |
-
|
| 8 |
-
Chapter 6, Section 6.20 of the notes. Trained on the four Gaussian blobs of
|
| 9 |
-
``cis6270.data.gaussian_mixture_2d``. Prints the doubling worked on dx/dt = x, where two steps of
|
| 10 |
-
0.25 average to 1.297443 and the direct step of 0.5 returns the same number, then the dyadic ladder
|
| 11 |
-
and the trained model's doubling residual at every rung.
|
| 12 |
-
|
| 13 |
-
python lecture_6/shortcut.py # 2000 steps, about twenty seconds
|
| 14 |
-
python lecture_6/shortcut.py --steps 20 # a few seconds, for a first look
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
# %% 01. Imports and configuration
|
| 18 |
-
import math
|
| 19 |
-
|
| 20 |
-
import torch
|
| 21 |
-
|
| 22 |
-
from cis6270.data import gaussian_mixture_2d
|
| 23 |
-
from cis6270.metrics import mean_log_density, mode_report
|
| 24 |
-
from cis6270.nets import TwoTimeMLP, count_parameters
|
| 25 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed, train
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
torch.set_num_threads(1) # These networks are small, and splitting one matrix product costs more
|
| 29 |
-
# than it saves. ``tests/conftest.py`` sets the same thing for the test session, so the line
|
| 30 |
-
# here is the one that acts when a reader runs this script directly.
|
| 31 |
-
|
| 32 |
-
FINEST = 1.0 / 128.0 # d_0, the finest step of the ladder.
|
| 33 |
-
LEVELS = 7 # log2(128) doublings, so the step sizes are d_k = 2^k d_0 for k = 0 to 7.
|
| 34 |
-
ZERO_FRACTION = 0.75 # Three quarters of each batch is drawn at step size zero.
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
# %% 02. The network in step-size coordinates
|
| 38 |
-
#
|
| 39 |
-
# The substitution s_theta(x, t, d) = v_{t, t+d}(x) turns the published network into the two-time
|
| 40 |
-
# average velocity of this chapter, so the same weights serve both readings.
|
| 41 |
-
def shortcut(model, x, t, d):
|
| 42 |
-
"""s_theta(x, t, d), the average velocity over the step of length d starting at time t."""
|
| 43 |
-
return model(x, t, t + d) # [B, 2] = v_{t, t+d}(x).
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def take_step(model, x, t, d):
|
| 47 |
-
"""One step of length d, which is the flow map X_{t, t+d} in these coordinates."""
|
| 48 |
-
return x + d[:, None] * shortcut(model, x, t, d) # [B, 2].
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
# %% 03. The doubling identity, worked on the scalar flow
|
| 52 |
-
#
|
| 53 |
-
# s(x, t, 2d) = half s(x, t, d) + half s(x', t+d, d) with x' = x + d s(x, t, d). On dx/dt = x the
|
| 54 |
-
# average velocity is x (e^d - 1)/d, and the identity holds at every step length at once.
|
| 55 |
-
def scalar_doubling(x_value: float = 1.0, d: float = 0.25):
|
| 56 |
-
"""Every term of the doubling identity on dx/dt = x, each one computed here."""
|
| 57 |
-
first = x_value * math.expm1(d) / d # s(1, 0, 0.25).
|
| 58 |
-
reached = x_value + d * first # x', which is e^{0.25} to six figures.
|
| 59 |
-
second = reached * math.expm1(d) / d # s(x', 0.25, 0.25), faster because the state has grown.
|
| 60 |
-
doubled = x_value * math.expm1(2.0 * d) / (2.0 * d) # s(1, 0, 0.5), computed directly.
|
| 61 |
-
return {
|
| 62 |
-
"s(1, 0, d)": first,
|
| 63 |
-
"state after the first step": reached,
|
| 64 |
-
"s(x', d, d)": second,
|
| 65 |
-
"average of the two": 0.5 * (first + second),
|
| 66 |
-
"s(1, 0, 2d) direct": doubled,
|
| 67 |
-
"doubling gap": abs(0.5 * (first + second) - doubled),
|
| 68 |
-
"state after the doubled step": x_value + 2.0 * d * doubled,
|
| 69 |
-
"exp(2d)": math.exp(2.0 * d),
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
# %% 04. The dyadic ladder
|
| 74 |
-
#
|
| 75 |
-
# Eight step sizes from 1/128 up to 1, each training pair tying level k to level k+1. A state at
|
| 76 |
-
# level zero is already trained against the diagonal by the anchor, so the correctness of the
|
| 77 |
-
# one-step map rests on a chain of seven relations and one regression.
|
| 78 |
-
def ladder():
|
| 79 |
-
"""The step sizes d_k = 2^k d_0, from the finest to the whole interval."""
|
| 80 |
-
return [FINEST * 2.0 ** k for k in range(LEVELS + 1)] # [1/128, ..., 1.0].
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
# %% 05. The loss
|
| 84 |
-
#
|
| 85 |
-
# At step size zero the target is the flow matching target of Chapter 2, and above it the batch
|
| 86 |
-
# draws a level of the ladder, forms the two short steps and regresses the doubled step onto their
|
| 87 |
-
# average, with the target detached. The chapter puts three quarters of the batch on the zero step
|
| 88 |
-
# and the rest on the doubling. Both terms are evaluated on the whole batch here and weighted in
|
| 89 |
-
# that proportion, which has the same expectation at one network call more per step.
|
| 90 |
-
def make_loss(model, data, batch_size, device):
|
| 91 |
-
sizes = torch.tensor(ladder()[:LEVELS], device=device) # Levels whose doubling still fits in [0,1].
|
| 92 |
-
|
| 93 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 94 |
-
index = torch.randint(0, len(data), (batch_size,), device=device)
|
| 95 |
-
x1 = data[index] # [B, 2] the data draw.
|
| 96 |
-
x0 = torch.randn_like(x1) # [B, 2] the source draw.
|
| 97 |
-
t = torch.rand(batch_size, device=device) # [B] a time for the zero-step rows.
|
| 98 |
-
xt = (1 - t[:, None]) * x0 + t[:, None] * x1 # [B, 2] the interpolant.
|
| 99 |
-
zero = torch.zeros(batch_size, device=device)
|
| 100 |
-
anchor = ((shortcut(model, xt, t, zero) - (x1 - x0)) ** 2).mean() # Step size zero.
|
| 101 |
-
|
| 102 |
-
d = sizes[torch.randint(0, LEVELS, (batch_size,), device=device)] # [B] one rung each.
|
| 103 |
-
start = (1.0 - 2.0 * d) * torch.rand(batch_size, device=device) # [B] t with t + 2d <= 1.
|
| 104 |
-
xs = (1 - start[:, None]) * x0 + start[:, None] * x1 # [B, 2] the interpolant at t.
|
| 105 |
-
with torch.no_grad(): # The two legs are frozen; only the doubled step is corrected.
|
| 106 |
-
first = shortcut(model, xs, start, d)
|
| 107 |
-
reached = xs + d[:, None] * first
|
| 108 |
-
second = shortcut(model, reached, start + d, d)
|
| 109 |
-
target = 0.5 * (first + second)
|
| 110 |
-
doubled = ((shortcut(model, xs, start, 2.0 * d) - target) ** 2).mean()
|
| 111 |
-
return ZERO_FRACTION * anchor + (1.0 - ZERO_FRACTION) * doubled
|
| 112 |
-
|
| 113 |
-
return loss_fn
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
# %% 06. Sampling, and the residual at every rung
|
| 117 |
-
@torch.no_grad()
|
| 118 |
-
def sample(model, x0, steps: int):
|
| 119 |
-
"""Cross [0, 1] in equal steps of length 1/steps, one network evaluation per step."""
|
| 120 |
-
x, d = x0, torch.full((len(x0),), 1.0 / steps, device=x0.device)
|
| 121 |
-
for k in range(steps):
|
| 122 |
-
t = torch.full((len(x0),), k / steps, device=x0.device)
|
| 123 |
-
x = take_step(model, x, t, d)
|
| 124 |
-
return x # [N, 2].
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
@torch.no_grad()
|
| 128 |
-
def rung_residuals(model, x0):
|
| 129 |
-
"""The squared doubling gap at each level of the ladder, measured on source points."""
|
| 130 |
-
out = {}
|
| 131 |
-
for k, d in enumerate(ladder()[:LEVELS]):
|
| 132 |
-
step = torch.full((len(x0),), d, device=x0.device)
|
| 133 |
-
start = torch.zeros(len(x0), device=x0.device)
|
| 134 |
-
first = shortcut(model, x0, start, step)
|
| 135 |
-
second = shortcut(model, x0 + step[:, None] * first, start + step, step)
|
| 136 |
-
gap = shortcut(model, x0, start, 2.0 * step) - 0.5 * (first + second)
|
| 137 |
-
out[f"rung {k}: d = 1/{int(round(1.0 / d))}"] = float((gap ** 2).mean())
|
| 138 |
-
return out
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
# %% 07. The report
|
| 142 |
-
def main() -> None:
|
| 143 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 144 |
-
parser.set_defaults(steps=2000, batch_size=128, samples=512)
|
| 145 |
-
args = parser.parse_args()
|
| 146 |
-
set_seed(args.seed)
|
| 147 |
-
device = pick_device(args.device)
|
| 148 |
-
data = gaussian_mixture_2d(num_points=2048, seed=args.seed).to(device) # [2048, 2].
|
| 149 |
-
|
| 150 |
-
report = scalar_doubling()
|
| 151 |
-
report["ladder levels"] = LEVELS
|
| 152 |
-
report["finest step d_0"] = FINEST
|
| 153 |
-
if not args.quiet:
|
| 154 |
-
print(f"on dx/dt = x: two steps of 0.25 average to {report['average of the two']:.6f}, "
|
| 155 |
-
f"one step of 0.5 returns {report['s(1, 0, 2d) direct']:.6f}")
|
| 156 |
-
print(f"ladder {[round(d, 5) for d in ladder()]}")
|
| 157 |
-
|
| 158 |
-
model = TwoTimeMLP(dim=2, width=args.width).to(device)
|
| 159 |
-
if not args.quiet:
|
| 160 |
-
print(f"\ntraining the shortcut model parameters={count_parameters(model):,}")
|
| 161 |
-
losses = train(model, make_loss(model, data, args.batch_size, device),
|
| 162 |
-
steps=args.steps, lr=args.lr, quiet=args.quiet)
|
| 163 |
-
|
| 164 |
-
torch.manual_seed(args.seed + 1)
|
| 165 |
-
x0 = torch.randn(args.samples, 2, device=device) # [N, 2] the shared source points.
|
| 166 |
-
report.update(rung_residuals(model, x0))
|
| 167 |
-
samples: list[str] = []
|
| 168 |
-
for steps in (1, 2, 4, 8):
|
| 169 |
-
points = sample(model, x0, steps)
|
| 170 |
-
stats = mode_report(points)
|
| 171 |
-
report[f"{steps}-step nearest distance"] = stats["nearest_distance"]
|
| 172 |
-
report[f"{steps}-step log density"] = mean_log_density(points.cpu())
|
| 173 |
-
samples.append(f"{steps} step(s): nearest {stats['nearest_distance']:.3f} "
|
| 174 |
-
f"modes {stats['modes_found']} entropy {stats['mode_entropy']:.3f}")
|
| 175 |
-
report["one step against eight"] = float(
|
| 176 |
-
(sample(model, x0, 1) - sample(model, x0, 8)).norm(dim=1).mean())
|
| 177 |
-
report["final loss"] = sum(losses[-20:]) / len(losses[-20:])
|
| 178 |
-
|
| 179 |
-
print_report("Shortcut models, Chapter 6", report, samples)
|
| 180 |
-
save_run(args.out, model=model, config=vars(args), report=report, losses=losses, samples=samples)
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
if __name__ == "__main__":
|
| 184 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_6/stochastic_map.py
DELETED
|
@@ -1,218 +0,0 @@
|
|
| 1 |
-
"""Strong stochastic flow maps, where the driving path is an argument of the map.
|
| 2 |
-
|
| 3 |
-
A map that matches only the transition kernel draws fresh noise on every leg, so one full step and
|
| 4 |
-
two half steps follow different trajectories and their composition residual has a floor at twice
|
| 5 |
-
the variance of the interval. A strong map takes the path itself, projected onto the first few
|
| 6 |
-
shifted Legendre polynomials, and the Chen relations build the coefficients of a long interval
|
| 7 |
-
from the coefficients of its halves, so all three legs of the cocycle are driven by one
|
| 8 |
-
realization.
|
| 9 |
-
|
| 10 |
-
Chapter 6, Section 6.31 of the notes. Runs on the linear equation dX = X dt + sigma dW
|
| 11 |
-
and on a cloud of starting states from ``cis6270.data.gaussian_mixture_2d``. Prints the weak
|
| 12 |
-
map's floor of 2K_1 = 6.389, the Legendre variances 1, 1/3 and 1/5, the Chen fusion that turns
|
| 13 |
-
four half-interval draws into (0.50, -0.20), and the pathwise error against the coefficient count.
|
| 14 |
-
|
| 15 |
-
python lecture_6/stochastic_map.py # 400,000 Monte Carlo draws, a few seconds
|
| 16 |
-
python lecture_6/stochastic_map.py --steps 20 # the floor of 1,000 draws, for a first look
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
# %% 01. Imports and configuration
|
| 20 |
-
import math
|
| 21 |
-
|
| 22 |
-
import torch
|
| 23 |
-
|
| 24 |
-
from cis6270.data import gaussian_mixture_2d
|
| 25 |
-
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed
|
| 26 |
-
|
| 27 |
-
torch.set_num_threads(1) # The work here is elementwise, and one thread keeps the timing stable.
|
| 28 |
-
# ``tests/conftest.py`` sets the same thing for the test session, so the line here is the one
|
| 29 |
-
# that acts when a reader runs this script directly.
|
| 30 |
-
torch.set_default_dtype(torch.float64) # The checks below sit well under the float32 noise floor.
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
SIGMA = 1.0 # The noise level of dX = X dt + sigma dW.
|
| 34 |
-
ORDER = 3 # Coefficients carried per interval, the number used in practice.
|
| 35 |
-
FINEST = 32 # Subintervals the path is drawn on before the Chen relations fuse it upward.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
# %% 02. The transition kernel, and the weak map built from it
|
| 39 |
-
#
|
| 40 |
-
# Starting at x at time s, the state at t is e^{t-s} x plus a centered Gaussian whose variance is
|
| 41 |
-
# K_{t-s}. A weak map reproduces that law on every interval with one fresh draw.
|
| 42 |
-
def kernel_variance(d):
|
| 43 |
-
"""K_d = sigma^2 (e^{2d} - 1)/2, the variance the equation accumulates over a length d."""
|
| 44 |
-
return 0.5 * SIGMA ** 2 * (math.exp(2.0 * d) - 1.0)
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def weak_step(x, d, noise):
|
| 48 |
-
"""The weak map, which takes a state and one fresh standard normal draw."""
|
| 49 |
-
return math.exp(d) * x + math.sqrt(kernel_variance(d)) * noise
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
# %% 03. The cost of matching only the transition kernel
|
| 53 |
-
#
|
| 54 |
-
# The variances compose correctly, so nothing is wrong at the level of distributions. At the level
|
| 55 |
-
# of samples the three draws are independent, so the squared gap between one full step and two
|
| 56 |
-
# half steps averages to twice the full-interval variance however well the map is trained.
|
| 57 |
-
def weak_floor(x: float = 0.3, u: float = 0.5, t: float = 1.0, draws: int = 400_000, seed: int = 0):
|
| 58 |
-
"""The worked comparison of Section 6.31, and a Monte Carlo average of the floor."""
|
| 59 |
-
full = weak_step(x, t, 0.8) # One step with the chapter's third draw.
|
| 60 |
-
half = weak_step(weak_step(x, u, 0.5), t - u, -1.2) # Two legs with the first two draws.
|
| 61 |
-
carried = math.exp(2.0 * (t - u)) * kernel_variance(u) + kernel_variance(t - u)
|
| 62 |
-
matching = (math.exp(t - u) * math.sqrt(kernel_variance(u)) * 0.5
|
| 63 |
-
- math.sqrt(kernel_variance(t - u)) * 1.2) / math.sqrt(kernel_variance(t))
|
| 64 |
-
generator = torch.Generator().manual_seed(seed)
|
| 65 |
-
e1, e2, e3 = (torch.randn(draws, generator=generator) for _ in range(3))
|
| 66 |
-
gap = (math.sqrt(kernel_variance(t)) * e3
|
| 67 |
-
- math.exp(t - u) * math.sqrt(kernel_variance(u)) * e1
|
| 68 |
-
- math.sqrt(kernel_variance(t - u)) * e2) # [draws] the difference of the two routes.
|
| 69 |
-
return {
|
| 70 |
-
"K_1": kernel_variance(t), "sqrt K_1": math.sqrt(kernel_variance(t)),
|
| 71 |
-
"sqrt K_0.5": math.sqrt(kernel_variance(u)),
|
| 72 |
-
"one full step": full, "two half steps": half, "gap": full - half,
|
| 73 |
-
"squared gap": (full - half) ** 2,
|
| 74 |
-
"carried variance equals K_1": carried, "floor 2 K_1": 2.0 * kernel_variance(t),
|
| 75 |
-
"the draw that would close it": matching,
|
| 76 |
-
"Monte Carlo floor": float((gap ** 2).mean()),
|
| 77 |
-
}
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
# %% 04. The path as an input, through the shifted Legendre polynomials
|
| 81 |
-
#
|
| 82 |
-
# I^{(n)}_{s,t} integrates the n-th shifted Legendre polynomial of the normalized time against the
|
| 83 |
-
# increment. The zeroth coefficient is the plain increment, the first separates a path that did
|
| 84 |
-
# its travelling early from one that did it late, and distinct coefficients are independent.
|
| 85 |
-
def legendre(n: int, r):
|
| 86 |
-
"""The n-th shifted Legendre polynomial on [0, 1]."""
|
| 87 |
-
return [torch.ones_like(r), 2.0 * r - 1.0, 6.0 * r * r - 6.0 * r + 1.0][n]
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def draw_coefficients(shape, length: float, generator):
|
| 91 |
-
"""I^{(n)} ~ N(0, (t-s)/(2n+1)), drawn directly, with no path simulated."""
|
| 92 |
-
scale = torch.tensor([math.sqrt(length / (2 * n + 1)) for n in range(ORDER)]) # [ORDER].
|
| 93 |
-
return torch.randn(*shape, ORDER, generator=generator) * scale # [..., ORDER].
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def kernel_coefficients(length: float, nodes: int = 8001):
|
| 97 |
-
"""a_n(h) in e^{h(1-r)} = sum_n a_n P_n(r), which turns the Ito integral into a finite sum."""
|
| 98 |
-
r = torch.linspace(0.0, 1.0, nodes)
|
| 99 |
-
weight = torch.exp(length * (1.0 - r)) # The kernel e^{t-u} written in normalized time.
|
| 100 |
-
return torch.stack([(2 * n + 1) * torch.trapz(weight * legendre(n, r), r)
|
| 101 |
-
for n in range(ORDER)]) # [ORDER].
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
def strong_step(x, length: float, coefficients):
|
| 105 |
-
"""The solution map over one interval, driven by the coefficients of the path on it."""
|
| 106 |
-
kernel = kernel_coefficients(length).to(x) # [ORDER].
|
| 107 |
-
return math.exp(length) * x + SIGMA * (coefficients * kernel).sum(-1) # Same shape as x.
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
# %% 05. The Chen relations
|
| 111 |
-
#
|
| 112 |
-
# A shifted Legendre polynomial of the long interval expands in the shifted Legendre basis of each
|
| 113 |
-
# half, so the coefficients fuse by a fixed linear rule with signs alternating in the parity of
|
| 114 |
-
# n + m. The first rule says the increments add, and the minus sign in the second is the oddness
|
| 115 |
-
# of P_1 about the midpoint.
|
| 116 |
-
def chen(first, second):
|
| 117 |
-
"""Fuse the coefficients of two equal halves into the coefficients of their union."""
|
| 118 |
-
return torch.stack([
|
| 119 |
-
first[..., 0] + second[..., 0], # The increments add.
|
| 120 |
-
-0.5 * (first[..., 0] - second[..., 0]) + 0.5 * (first[..., 1] + second[..., 1]),
|
| 121 |
-
0.25 * (first[..., 2] + second[..., 2]) - 0.75 * (first[..., 1] - second[..., 1]),
|
| 122 |
-
], dim=-1)
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def fuse_to(pieces, target: int):
|
| 126 |
-
"""Halve the number of intervals by Chen fusion until ``target`` intervals remain."""
|
| 127 |
-
while pieces.shape[-2] > target:
|
| 128 |
-
pieces = chen(pieces[..., 0::2, :], pieces[..., 1::2, :]) # Fuse neighbouring pairs.
|
| 129 |
-
return pieces # [..., target, ORDER].
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
def worked_chen():
|
| 133 |
-
"""The chapter's four half-interval draws, fused into the coefficients of the unit interval."""
|
| 134 |
-
halves = torch.tensor([[0.80, 0.20, 0.0], [-0.30, 0.50, 0.0]]) # [2, ORDER], the two legs.
|
| 135 |
-
fused = chen(halves[0], halves[1])
|
| 136 |
-
return {"I0 over [0,1]": float(fused[0]), "I1 over [0,1]": float(fused[1]),
|
| 137 |
-
"variance of I1 at h=1": 1.0 / 3.0,
|
| 138 |
-
"fused variance at h=1": 0.25 * 0.5 + 0.25 * 0.5 + 0.25 / 6.0 + 0.25 / 6.0}
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
# %% 06. The pathwise error against the number of coefficients
|
| 142 |
-
#
|
| 143 |
-
# A fine simulation gives both the exact Ito integral and the coefficients of the same path, so
|
| 144 |
-
# the truncation can be measured on the realization it was taken from.
|
| 145 |
-
def truncation_error(paths: int, grid: int, seed: int):
|
| 146 |
-
"""Mean absolute pathwise error of a one-step map reading 1, 2 and 3 coefficients."""
|
| 147 |
-
generator = torch.Generator().manual_seed(seed)
|
| 148 |
-
step = 1.0 / grid
|
| 149 |
-
time = torch.arange(grid) * step # [grid] left endpoints of the substeps.
|
| 150 |
-
increments = torch.randn(paths, grid, generator=generator) * math.sqrt(step) # [P, grid].
|
| 151 |
-
exact = (torch.exp(1.0 - time)[None, :] * increments).sum(1) # [P] the Ito integral.
|
| 152 |
-
measured = torch.stack([(legendre(n, time)[None, :] * increments).sum(1)
|
| 153 |
-
for n in range(ORDER)], dim=1) # [P, ORDER].
|
| 154 |
-
kernel = kernel_coefficients(1.0) # [ORDER].
|
| 155 |
-
out = {f"coefficient variance I{n}": float(measured[:, n].var()) for n in range(ORDER)}
|
| 156 |
-
for keep in range(1, ORDER + 1):
|
| 157 |
-
approximate = (measured[:, :keep] * kernel[:keep]).sum(1) # [P].
|
| 158 |
-
out[f"pathwise error with {keep}"] = float((exact - approximate).abs().mean())
|
| 159 |
-
return out
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
# %% 07. One path, several step counts
|
| 163 |
-
#
|
| 164 |
-
# A strong map crosses one trajectory of one realization in any number of hops, so its terminal
|
| 165 |
-
# state barely moves with the step count. A weak map redraws at every change of the step count,
|
| 166 |
-
# so its terminal state moves by the floor of Section 6.31.
|
| 167 |
-
def cross(x, coefficients, steps: int, weak: bool, generator=None):
|
| 168 |
-
"""Cross [0, 1] in ``steps`` equal hops, either along the fused path or with fresh draws."""
|
| 169 |
-
pieces = fuse_to(coefficients, steps) # [..., steps, ORDER] on this grid.
|
| 170 |
-
length = 1.0 / steps
|
| 171 |
-
for k in range(steps):
|
| 172 |
-
if weak:
|
| 173 |
-
noise = torch.randn(x.shape, generator=generator) # A fresh draw for every leg.
|
| 174 |
-
x = weak_step(x, length, noise)
|
| 175 |
-
else:
|
| 176 |
-
x = strong_step(x, length, pieces[..., k, :])
|
| 177 |
-
return x
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
# %% 08. The report
|
| 181 |
-
def main() -> None:
|
| 182 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 183 |
-
parser.set_defaults(steps=400_000, samples=512)
|
| 184 |
-
args = parser.parse_args() # --steps is the Monte Carlo draw count here, since nothing trains.
|
| 185 |
-
set_seed(args.seed)
|
| 186 |
-
pick_device(args.device) # Every number here is double precision, which not every
|
| 187 |
-
# accelerator carries, so the work stays on the CPU whatever --device names.
|
| 188 |
-
|
| 189 |
-
report = weak_floor(draws=max(args.steps, 1000), seed=args.seed)
|
| 190 |
-
report.update(worked_chen())
|
| 191 |
-
report.update(truncation_error(paths=1024, grid=4096, seed=args.seed))
|
| 192 |
-
if not args.quiet:
|
| 193 |
-
print(f"weak floor 2K_1 = {report['floor 2 K_1']:.3f}, Monte Carlo "
|
| 194 |
-
f"{report['Monte Carlo floor']:.3f}")
|
| 195 |
-
print(f"pathwise error {report['pathwise error with 1']:.4f} with the increment alone "
|
| 196 |
-
f"against {report['pathwise error with 3']:.6f} with three coefficients")
|
| 197 |
-
|
| 198 |
-
# %% 09. The same cloud at four step counts
|
| 199 |
-
points = gaussian_mixture_2d(num_points=args.samples, seed=args.seed).double() # [N, 2].
|
| 200 |
-
generator = torch.Generator().manual_seed(args.seed + 1)
|
| 201 |
-
path = draw_coefficients((args.samples, 2, FINEST), 1.0 / FINEST, generator) # [N, 2, F, 3].
|
| 202 |
-
reference = cross(points, path, FINEST, weak=False) # The finest crossing, as the yardstick.
|
| 203 |
-
samples: list[str] = []
|
| 204 |
-
for steps in (2, 4, 8, 16): # Short of FINEST, which would be the yardstick against itself.
|
| 205 |
-
strong = cross(points, path, steps, weak=False) # One realization, several hop counts.
|
| 206 |
-
weak_generator = torch.Generator().manual_seed(args.seed + 2)
|
| 207 |
-
weak = cross(points, path, steps, weak=True, generator=weak_generator)
|
| 208 |
-
report[f"{steps} hops: strong spread"] = float((strong - reference).norm(dim=1).mean())
|
| 209 |
-
report[f"{steps} hops: weak spread"] = float((weak - reference).norm(dim=1).mean())
|
| 210 |
-
samples.append(f"{steps:2d} hops strong {float((strong - reference).norm(dim=1).mean()):.6f}"
|
| 211 |
-
f" weak {float((weak - reference).norm(dim=1).mean()):.3f}")
|
| 212 |
-
|
| 213 |
-
print_report("Strong stochastic flow maps, Chapter 6", report, samples)
|
| 214 |
-
save_run(args.out, config=vars(args), report=report, samples=samples)
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
if __name__ == "__main__":
|
| 218 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|