Delete lecture_7
Browse files- lecture_7/README.md +0 -166
- lecture_7/branched.py +0 -344
- lecture_7/csbm.py +0 -254
- lecture_7/ddsbm.py +0 -243
- lecture_7/discrete_bridge.py +0 -267
- lecture_7/displacement.py +0 -237
- lecture_7/dsb.py +0 -204
- lecture_7/dsbm.py +0 -207
- lecture_7/dynamic_bridge.py +0 -257
- lecture_7/optimal_transport.py +0 -323
- lecture_7/ot_flow_matching.py +0 -215
- lecture_7/sf2m.py +0 -248
- lecture_7/sinkhorn.py +0 -234
- lecture_7/static_bridge.py +0 -295
lecture_7/README.md
DELETED
|
@@ -1,166 +0,0 @@
|
|
| 1 |
-
# Lecture 7: Optimal Transport and Schrodinger Bridges
|
| 2 |
-
|
| 3 |
-
Chapter 7 of the notes.
|
| 4 |
-
|
| 5 |
-
Every model of Chapters 2 through 6 trains on a pair of samples, and in almost all of them the two
|
| 6 |
-
were drawn independently. This lecture takes the pairing as the quantity to be chosen. The first
|
| 7 |
-
four files price a pairing, relax Monge's map to Kantorovich's coupling, certify the optimum with
|
| 8 |
-
prices, smooth the problem with an entropy term that Sinkhorn's algorithm solves by rescaling rows
|
| 9 |
-
and columns, and read transport as motion in time, where the minimizer moves every particle in a
|
| 10 |
-
straight line. The next five replace the cost matrix by the endpoint law of a reference process,
|
| 11 |
-
which turns the same scaling into a Schrodinger bridge: the chain rule puts reference bridges
|
| 12 |
-
between every pair of ends, Doob's h-transform makes the bridge a process we can run forward, and
|
| 13 |
-
three learning methods differ only in what they simulate and what they regress. Three more carry
|
| 14 |
-
the construction to a finite state space, where a rate matrix replaces the drift, where a molecular
|
| 15 |
-
graph factorizes into forty-five categorical features, and where discrete time makes the whole path
|
| 16 |
-
law a table of sixteen numbers. The last file covers a population that splits into weighted branches
|
| 17 |
-
and a system of interacting particles whose own dynamics is the reference.
|
| 18 |
-
|
| 19 |
-
Almost every number below is computed in closed form, which is what the chapter does. Two files
|
| 20 |
-
train.
|
| 21 |
-
|
| 22 |
-
| file | command | what it computes |
|
| 23 |
-
| ---- | ------- | ---------------- |
|
| 24 |
-
| `optimal_transport.py` | `python lecture_7/optimal_transport.py` | the four points, the two-by-two plan and its prices, the sorted pairing on three points, and the Gaussian map |
|
| 25 |
-
| `sinkhorn.py` | `python lecture_7/sinkhorn.py` | the Gibbs kernel, the first sweep by hand, the converged entropic plan, and the plan at four values of epsilon |
|
| 26 |
-
| `displacement.py` | `python lecture_7/displacement.py` | the kinetic action of one particle and of a population, the displacement interpolation, and its velocity field |
|
| 27 |
-
| `ot_flow_matching.py` | `python lecture_7/ot_flow_matching.py` | **trains** two velocity networks on the four-Gaussian target, one per coupling, and measures the straightness of each |
|
| 28 |
-
| `static_bridge.py` | `python lecture_7/static_bridge.py` | the eight path probabilities, the chain rule, proportional fitting, the tilted matrices, and the bridge path law |
|
| 29 |
-
| `dynamic_bridge.py` | `python lecture_7/dynamic_bridge.py` | Girsanov on a constant control, the backward and Hamilton-Jacobi-Bellman residuals, and the Brownian bridge |
|
| 30 |
-
| `dsb.py` | `python lecture_7/dsb.py` | the two half-steps of proportional fitting on path laws, and the diffusion reversal on a Gaussian |
|
| 31 |
-
| `dsbm.py` | `python lecture_7/dsbm.py` | the averaged bridge drift at one point, and one round of Markovian fitting on the chain |
|
| 32 |
-
| `sf2m.py` | `python lecture_7/sf2m.py` | the two closed-form regression targets at a bridge sample, and the entropic plan of a batch |
|
| 33 |
-
| `discrete_bridge.py` | `python lecture_7/discrete_bridge.py` | the two-state continuous-time bridge, its Euler sampler and its reverse rates, and the cost computed twice |
|
| 34 |
-
| `ddsbm.py` | `python lecture_7/ddsbm.py` | the single bond with three types, the rates pinned at a terminal graph, and the counting over forty-five features |
|
| 35 |
-
| `csbm.py` | `python lecture_7/csbm.py` | the two projections in discrete time, and D-IMF on sixteen paths against the reference's mixing |
|
| 36 |
-
| `branched.py` | `python lecture_7/branched.py` | the two-branch energies, the tilt of a reference, and **trains** a bias force on two interacting particles |
|
| 37 |
-
|
| 38 |
-
Every file takes `--seed` and `--quiet`, and `--steps` means the quantity that file iterates: the
|
| 39 |
-
number of quantile levels, Sinkhorn sweeps, times on a grid, optimizer steps, fitting rounds or
|
| 40 |
-
Euler steps. The slowest default run is about seven seconds; all thirteen run on a laptop CPU with
|
| 41 |
-
no download.
|
| 42 |
-
|
| 43 |
-
## What a default run produces
|
| 44 |
-
|
| 45 |
-
`optimal_transport.py`. The ordered pairing of the four points costs 1 against 5 for the crossed
|
| 46 |
-
one and 3 for the independent coupling. The two-by-two optimum is `[[0.3, 0.3], [0, 0.4]]` at cost
|
| 47 |
-
3.4, the prices `f = (0, -8)` and `g = (1, 9)` have slack `[[0, 0], [8, 0]]` and dual value 3.4, so
|
| 48 |
-
the duality gap is zero and `W_2 = 1.8439`. The six permutations of the three-point example cost
|
| 49 |
-
11/3, 17/3, 35/3, 15, 77/3 and 27, and the quantile integral on 3072 levels returns 3.6667.
|
| 50 |
-
Transporting N(0,1) to N(3,4) gives `W_2^2 = 10` under the map `T(x) = 3 + 2x`, and the same map on
|
| 51 |
-
a stratified sample gives 9.9996.
|
| 52 |
-
|
| 53 |
-
`sinkhorn.py`. The kernel is `[[0.606531, 0.011109], [0.606531, 0.606531]]`. The first sweep gives
|
| 54 |
-
`u = (0.971440, 0.329744)` and `v = (0.38013, 3.32081)`, and the marginal error falls 0.680, 0.382,
|
| 55 |
-
0.155, 0.047, 0.012, reaching 1e-9 after 17 sweeps. The entropic plan is
|
| 56 |
-
`[[0.293122, 0.306878], [0.006878, 0.393122]]` at cost 3.45502 against 3.4, and the regularized
|
| 57 |
-
objective is 1.208 at it, 1.222 at the exact optimum and 1.792 at independence. At epsilon = 10,
|
| 58 |
-
2, 0.5 and 0.1 the cost is 4.0540165, 3.4550204, 3.4000004 and 3.4000000, in 7, 17, 28 and 80
|
| 59 |
-
sweeps.
|
| 60 |
-
|
| 61 |
-
`displacement.py`. A steady particle has action 4 and a dawdling one 8, for the same endpoints. The
|
| 62 |
-
average squared displacement is 1, 3 and 5 under the ordered, independent and crossed pairings. At
|
| 63 |
-
t = 1/2 the displacement interpolation is N(1.5, 2.25) and the mixture of the two ends peaks at
|
| 64 |
-
0.13, with total variation 0.257 between them, and `W_2(rho_0, rho_1/2) = 1.5811`, half of 3.1623.
|
| 65 |
-
The straight paths have action 9.9987, which is their average squared displacement. The action
|
| 66 |
-
density is 9.9994 at every time on the grid, the stratified estimate of the exact 10, and the
|
| 67 |
-
continuity residual is 1.1e-16.
|
| 68 |
-
|
| 69 |
-
`ot_flow_matching.py`. On the two-point batch the independent coupling has average squared velocity
|
| 70 |
-
3 and the optimal one 1. Over training the batch coupling cost is 10.31 against 3.25, the learned
|
| 71 |
-
paths have straightness 1.80 against 0.065, and the one-step error is 2.56 against 0.41. Both
|
| 72 |
-
models find all four modes. At `--steps 1500`, about eighteen seconds, the optimal coupling reaches
|
| 73 |
-
straightness 0.024 and one-step error 0.28 while the independent one does not improve.
|
| 74 |
-
|
| 75 |
-
`static_bridge.py`. The eight reference paths sum to one with `R(1,1,2) = 0.105`. The chain rule
|
| 76 |
-
splits 0.2180 into 0.0201 and 0.1979. The reference lands 0.73 away from the requested terminal
|
| 77 |
-
law, and proportional fitting reaches `f = (1.072002, 0.937019)` and `g = (0.350517, 1.843637)`,
|
| 78 |
-
with the terminal error falling by 187.5 per round. The bridge coupling is
|
| 79 |
-
`[[0.114605, 0.385395], [0.085395, 0.414605]]` at `KL = 0.2820` against 0.2847 for independence.
|
| 80 |
-
The tilted matrices carry (0.5, 0.5) to (0.449213, 0.550787) to (0.2, 0.8), the path ratios take
|
| 81 |
-
four values, and `KL(P* || R) = 0.281961`.
|
| 82 |
-
|
| 83 |
-
`dynamic_bridge.py`. The relative entropy of a constant control u = 2 is 2 from the control energy,
|
| 84 |
-
2 from the two endpoint laws and 1.9965 from the simulated paths. The backward residual is 2.2e-16
|
| 85 |
-
and the Hamilton-Jacobi-Bellman residual 8.9e-16, and `-grad V` equals the bridge drift to 4.4e-16.
|
| 86 |
-
At t = 0.25 the bridge has mean 0.5 and variance 0.1875, and at the state 0.6732 its drift is
|
| 87 |
-
1.7691 against the straight-line velocity 2. At t = 1/2 the simulated mean is 1.0095 and the
|
| 88 |
-
simulated variance 0.2563, against the exact 1 and 0.25.
|
| 89 |
-
|
| 90 |
-
`dsb.py`. The reversal of the last step is `[[0.681416, 0.318584], [0.379310, 0.620690]]`. The
|
| 91 |
-
backward chain started from (0.2, 0.8) reaches (0.466585, 0.533415), which is 0.0668 from the source
|
| 92 |
-
law, and the second half-step returns the terminal law (0.201967, 0.798033), off by 0.0039. That is
|
| 93 |
-
the round-1 error of proportional fitting on the coupling, and the error falls by 187.5 per round.
|
| 94 |
-
Run backward from its own terminal law, the diffusion reversal lands at variance 0.9836 against the
|
| 95 |
-
exact 1.
|
| 96 |
-
|
| 97 |
-
`dsbm.py`. Two bridges through x = 1 at t = 0.5 have drifts 4 and -2 and average 1, where the
|
| 98 |
-
derivative of the squared error vanishes. One round of Markovian fitting gives
|
| 99 |
-
`M_0 = [[0.591425, 0.408575], [0.307692, 0.692308]]` and
|
| 100 |
-
`M_1 = [[0.298457, 0.701543], [0.119588, 0.880412]]`, whose endpoint coupling has exact marginals
|
| 101 |
-
and brings the gap from 0.0584 to 0.0077. The gap then falls by 7.37 per round and nine rounds reach
|
| 102 |
-
1e-9.
|
| 103 |
-
|
| 104 |
-
`sf2m.py`. At t = 0.25 with z = 0.4 the sample is 0.6732, the drift 1.7691, the score -0.9238, the
|
| 105 |
-
flow target 2.2309 and the scaled score exactly -0.4. At t = 0.75 the coefficient changes sign and
|
| 106 |
-
the flow target is 1.7691, so the two lie symmetrically about the straight-line velocity 2.
|
| 107 |
-
|
| 108 |
-
`discrete_bridge.py`. Halving every rate costs 0.1534 and doubling costs 0.3863. The two-state
|
| 109 |
-
bridge has `g = (0.3349, 1.9643)`, `phi_1/2 = (0.7999, 1.2667)` and the generator
|
| 110 |
-
`[[-1.5836, 1.5836], [0.9472, -0.9472]]` at t = 1/2, where the marginal is (0.4668, 0.5332) and the
|
| 111 |
-
reverse rates 1.3862 and 1.0821 match the forward flux at 0.7392. The Euler sampler's terminal
|
| 112 |
-
error is 0.0223, 0.0106, 0.0051 and 0.0020 as the step is halved twice and then taken to a hundred
|
| 113 |
-
steps. The cost is 0.3235 on the endpoints and 0.3235 on the paths, with the integrand 0.1300 at
|
| 114 |
-
t = 1/2, and it rises 0.3148, 0.3235, 0.3337, 0.3348 toward the limit 0.3348 as the reference
|
| 115 |
-
speeds up.
|
| 116 |
-
|
| 117 |
-
`ddsbm.py`. The bond reference has `beta = 0.9163` and off-diagonal rate 0.3054, and the first
|
| 118 |
-
column scaling is (0.2083, 0.7143, 2.9167). Two hundred sweeps give `f = (1.1779, 0.9506, 0.5062)`
|
| 119 |
-
and `g = (0.1842, 0.6918, 3.0004)`, a plan that moves 0.4948 of the mass from no bond to double and
|
| 120 |
-
leaves 0.2612 unchanged, between 0.18 for independence and 0.4 for the edit-minimizing plan. At
|
| 121 |
-
r = 0.2, 0.4, 0.6, 0.8 and 0.99 the mass that stays is 0.2250, 0.2612, 0.2926, 0.3248 and 0.3812.
|
| 122 |
-
The survival ratio reaches 1/2 at t = 0.2435, where the pinned rates are 1.2217, 0.0764 and 0.3054,
|
| 123 |
-
a factor of 16 between a move onto the terminal type and a move off it, and one such edge against a
|
| 124 |
-
network rate of 1 contributes 0.0229. With two terminal types the network learns 0.8552 at a loss
|
| 125 |
-
of 0.1357, against 0.1467 at 1 and 0.2395 at 0.5. Nine heavy atoms give 45 features and 9.2e+27
|
| 126 |
-
graphs, at a total jump rate of 24.74 per unit time.
|
| 127 |
-
|
| 128 |
-
`csbm.py`. With alpha = 0.35 and three transitions, `A^3 = [[0.5135, 0.4865], [0.4865, 0.5135]]`
|
| 129 |
-
and the exact bridge coupling is `[[0.10432, 0.39568], [0.09568, 0.40432]]`. The path 1,1,1,1 has
|
| 130 |
-
reference bridge weight 0.5348, so its probability is 0.0535 at the start and 0.0558 under the
|
| 131 |
-
bridge. The two laws agree at the interior times, (0.4754, 0.5246) and (0.4107, 0.5893), and differ
|
| 132 |
-
by 0.0173 over the paths. One round of D-IMF brings that to 2.9e-04 and later rounds divide it by
|
| 133 |
-
58.9, reaching 2.4e-11 after five. The last conditional (0.3160, 0.6840) against a network at
|
| 134 |
-
(0.25, 0.75) costs 0.0110 nats. At alpha = 0.2, 0.35 and 0.4 the starting gap is 0.13589, 0.01728
|
| 135 |
-
and 0.00512 and the rounds divide it by 5, 59 and 284; on 8, 16 and 64 paths it is 0.05743, 0.01728
|
| 136 |
-
and 0.00156 and the rounds divide it by 7, 59 and 4674. The ordered reference out of the middle of
|
| 137 |
-
five categories is (0.0545, 0.2442, 0.4026, 0.2442, 0.0545).
|
| 138 |
-
|
| 139 |
-
`branched.py`. The two branches have weighted energies 1.7 and 0.3 for a total of 2.0, and with the
|
| 140 |
-
second target at -4 the linear schedule costs 2.9 against 2.6 for one that releases mass late. The
|
| 141 |
-
bent interpolant has energy 2.1667 against 2 for the straight line. The two-outcome tilt has
|
| 142 |
-
`Z = 0.109`, raises the chance of reaching B from 0.1 to 0.917, and costs 1.836 nats, with
|
| 143 |
-
`J(b*) = -log Z = 2.216`. Training the bias force raises the mean terminal reward from -21.9 under
|
| 144 |
-
the reference to -6.9. A longer run does not improve that, because the importance weights
|
| 145 |
-
concentrate as the control grows and the effective sample size falls with it.
|
| 146 |
-
|
| 147 |
-
## What is simplified
|
| 148 |
-
|
| 149 |
-
- Every example runs on the chapter's own two states, three bond types, four points or two
|
| 150 |
-
particles. Nothing here is a benchmark, and no file touches a real dataset.
|
| 151 |
-
- The minibatch coupling of `ot_flow_matching.py` is an exact assignment through
|
| 152 |
-
`scipy.optimize.linear_sum_assignment`, which needs the two sides to be the same size. The
|
| 153 |
-
chapter's general `n` by `m` problem with unequal masses appears only in `sinkhorn.py`.
|
| 154 |
-
- The two training files use small networks and short runs, so their reports show the direction of
|
| 155 |
-
an effect and not its converged size.
|
| 156 |
-
- `dsb.py` and `dsbm.py` carry out their projections exactly on the full table of paths. The
|
| 157 |
-
networks the chapter describes, which replace those projections by a regression when the state
|
| 158 |
-
space is large, are not trained here.
|
| 159 |
-
- `discrete_bridge.py` and `ddsbm.py` work one categorical feature at a time. `ddsbm.py` counts the
|
| 160 |
-
forty-five features of a molecular graph and the rate of the product process, and no graph is
|
| 161 |
-
built.
|
| 162 |
-
- `csbm.py` runs both projections exactly on the sixteen-entry table. The network that replaces the
|
| 163 |
-
conditionals, and the simulation that replaces the exact endpoint coupling, are not trained here.
|
| 164 |
-
- `branched.py` uses constant growth rates taken from the worked example, and trains only the bias
|
| 165 |
-
force of the entangled construction. The four-stage training of the branched construction and the
|
| 166 |
-
state cost `V_t` are not fitted.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/branched.py
DELETED
|
@@ -1,344 +0,0 @@
|
|
| 1 |
-
"""Two bridges that change the shape of the problem: branching, and interacting particles.
|
| 2 |
-
|
| 3 |
-
A population leaving one source does not always end in one place, and Branched Schrodinger Bridge
|
| 4 |
-
Matching gives each outcome its own controlled diffusion and its own share of the mass, with a
|
| 5 |
-
learned growth rate moving mass between branches. Entangled Schrodinger Bridge Matching keeps one
|
| 6 |
-
population and changes the reference, taking the molecular dynamics of the system itself and
|
| 7 |
-
tilting it at the final time by a target density, which a bias force on every particle reproduces.
|
| 8 |
-
|
| 9 |
-
Chapter 7, Sections 7.26 to 7.27.3 of the notes. Runs the two-branch example in closed form and
|
| 10 |
-
trains a bias force on two interacting particles in the plane. Prints the weighted energies of the
|
| 11 |
-
two branch schedules, the interpolant and its energy, the reweighting arithmetic of the two-outcome
|
| 12 |
-
example, one Euler-Maruyama step, and the reward the trained force reaches.
|
| 13 |
-
|
| 14 |
-
python lecture_7/branched.py # 100 optimizer steps on the bias force
|
| 15 |
-
python lecture_7/branched.py --steps 20 # a few seconds, for a first look
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
from __future__ import annotations
|
| 20 |
-
|
| 21 |
-
import math
|
| 22 |
-
|
| 23 |
-
import torch
|
| 24 |
-
import torch.nn as nn
|
| 25 |
-
|
| 26 |
-
from cis6270.nets import TimeEmbedding, count_parameters
|
| 27 |
-
from cis6270.runner import (
|
| 28 |
-
common_parser, pick_device, print_report, save_run, set_seed, train,
|
| 29 |
-
)
|
| 30 |
-
|
| 31 |
-
PARTICLES = 2 # n, the number of interacting particles.
|
| 32 |
-
DIMENSION = 2 # Each particle lives in the plane, so the projector below has something to remove.
|
| 33 |
-
MASS, FRICTION, TEMPERATURE = 1.0, 1.0, 1.0 # m_i, gamma and k_B tau, all in reduced units.
|
| 34 |
-
NOISE = math.sqrt(2.0 * FRICTION * TEMPERATURE / MASS) # The noise scale of the reference.
|
| 35 |
-
TARGET_WIDTH = 0.4 # The width of the target density around the region B.
|
| 36 |
-
PATHS = 64 # Trajectories simulated per optimizer step.
|
| 37 |
-
SIM_STEPS = 30 # Euler-Maruyama steps per trajectory.
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
# %% 02. A population that splits
|
| 41 |
-
#
|
| 42 |
-
# Branch k is a controlled diffusion carrying the fraction w^k_t of the population, and the weights
|
| 43 |
-
# change at a learned growth rate with sum_k g^k_t = 0, so a branch grows only by taking mass from
|
| 44 |
-
# another. Section 7.26.
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def branch_weights(growth, t: torch.Tensor) -> torch.Tensor:
|
| 48 |
-
"""w^1_t = integral of the growth rate from 0 to t, with branch 0 holding the remainder."""
|
| 49 |
-
return growth(t) # The weight of branch 1; branch 0 carries 1 - w^1_t.
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def weighted_energy(drift: float, weight, points: int = 2001) -> float:
|
| 53 |
-
"""The time integral of (1/2) ||u||^2 times the weight of the branch, by the trapezoid rule."""
|
| 54 |
-
t = torch.linspace(0.0, 1.0, points)
|
| 55 |
-
density = 0.5 * drift ** 2 * weight(t) # The control energy priced by the mass that spends it.
|
| 56 |
-
return float(torch.trapezoid(density, t))
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def branch_sample(weight_one: float, generator: torch.Generator) -> int:
|
| 60 |
-
"""Draw a branch with probability w^k_t, which is how a sample of the population is taken."""
|
| 61 |
-
return int(torch.rand(1, generator=generator) < weight_one) # 1 for branch 1, 0 for branch 0.
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
# %% 03. The interpolant
|
| 65 |
-
#
|
| 66 |
-
# A network correction multiplied by t(1 - t) leaves both endpoints fixed for every value of the
|
| 67 |
-
# parameters, so the endpoint constraints hold during training with no penalty term. With no state
|
| 68 |
-
# cost the energy of a bent path exceeds the energy of the straight line, and the training drives
|
| 69 |
-
# the correction to zero. Section 7.26.2.
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def interpolant(x0: float, x1: float, correction: float, t: torch.Tensor) -> torch.Tensor:
|
| 73 |
-
"""x_t = (1 - t) x_0 + t x_1 + t(1 - t) phi, with phi here a constant."""
|
| 74 |
-
return (1.0 - t) * x0 + t * x1 + t * (1.0 - t) * correction
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def interpolant_velocity(x0: float, x1: float, correction: float, t: torch.Tensor) -> torch.Tensor:
|
| 78 |
-
"""x_dot = (x_1 - x_0) + (1 - 2t) phi, the derivative of the interpolant in t."""
|
| 79 |
-
return (x1 - x0) + (1.0 - 2.0 * t) * correction
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def interpolant_energy(x0: float, x1: float, correction: float, points: int = 2001) -> float:
|
| 83 |
-
"""The integral of one half the squared velocity of the interpolant over the unit interval."""
|
| 84 |
-
t = torch.linspace(0.0, 1.0, points)
|
| 85 |
-
return float(torch.trapezoid(0.5 * interpolant_velocity(x0, x1, correction, t) ** 2, t))
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
# %% 04. The reference of Entangled Schrodinger Bridge Matching
|
| 89 |
-
#
|
| 90 |
-
# The reference is the dynamics the system actually follows, underdamped Langevin with the physical
|
| 91 |
-
# force, the friction and the thermal noise. Section 7.27.
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def potential(positions: torch.Tensor) -> torch.Tensor:
|
| 95 |
-
"""U(R), a double well on each particle plus a spring that couples the two. [B, n, d] -> [B]."""
|
| 96 |
-
radial = ((positions ** 2).sum(dim=-1) - 1.0) ** 2 # [B, n] a well at every unit circle.
|
| 97 |
-
coupling = 0.5 * ((positions[:, 0] - positions[:, 1]) ** 2).sum(dim=-1) # [B] the spring.
|
| 98 |
-
return radial.sum(dim=-1) + coupling # [B].
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def physical_force(positions: torch.Tensor) -> torch.Tensor:
|
| 102 |
-
"""-grad_R U(R), differentiated by hand so the simulation needs no autograd. [B, n, d]."""
|
| 103 |
-
radial = -4.0 * ((positions ** 2).sum(dim=-1, keepdim=True) - 1.0) * positions # [B, n, d].
|
| 104 |
-
separation = positions[:, 0] - positions[:, 1] # [B, d] the displacement across the spring.
|
| 105 |
-
spring = torch.stack([-separation, separation], dim=1) # [B, n, d] equal and opposite.
|
| 106 |
-
return radial + spring
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def force_check(positions: torch.Tensor) -> float:
|
| 110 |
-
"""The largest gap between the force written by hand and the gradient autograd computes."""
|
| 111 |
-
r = positions.detach().requires_grad_(True)
|
| 112 |
-
gradient = torch.autograd.grad(potential(r).sum(), r)[0]
|
| 113 |
-
return float((physical_force(positions) + gradient).abs().max())
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
TARGET = torch.tensor([1.0, 0.0]) # The centre of the region B, the same for both particles.
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def log_target(positions: torch.Tensor) -> torch.Tensor:
|
| 120 |
-
"""r(X_1) = log pi_B(R_1), zero inside the region and negative outside. [B, n, d] -> [B]."""
|
| 121 |
-
offset = positions - TARGET.to(positions) # [B, n, d] displacement from the target.
|
| 122 |
-
return -(offset ** 2).sum(dim=(1, 2)) / (2.0 * TARGET_WIDTH ** 2) # [B].
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def target_direction(positions: torch.Tensor) -> torch.Tensor:
|
| 126 |
-
"""s_hat_i, the unit vector along the gradient of log pi_B for particle i. [B, n, d]."""
|
| 127 |
-
gradient = -(positions - TARGET.to(positions)) / TARGET_WIDTH ** 2 # [B, n, d].
|
| 128 |
-
return gradient / gradient.norm(dim=-1, keepdim=True).clamp(min=1e-8)
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
# %% 05. The bias force and its parameterization
|
| 132 |
-
#
|
| 133 |
-
# A nonnegative scalar along the direction of steepest increase of the target density, plus a second
|
| 134 |
-
# output with its component along that direction removed, so the push toward the target is never
|
| 135 |
-
# negative and is read off directly as alpha. Section 7.27.3.
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
def compose_bias(alpha: torch.Tensor, h: torch.Tensor, s_hat: torch.Tensor) -> torch.Tensor:
|
| 139 |
-
"""b = alpha s_hat + (I - s_hat s_hat^T) h, the parameterization of the force on each particle."""
|
| 140 |
-
along = (h * s_hat).sum(dim=-1, keepdim=True) * s_hat # [B, n, d] the removed component.
|
| 141 |
-
return alpha.unsqueeze(-1) * s_hat + (h - along) # [B, n, d].
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
class BiasForce(nn.Module):
|
| 145 |
-
"""A network from the positions, the velocities and the time to one force per particle."""
|
| 146 |
-
|
| 147 |
-
def __init__(self, width: int = 64):
|
| 148 |
-
super().__init__()
|
| 149 |
-
self.time_embedding = TimeEmbedding(width, scale=1.0)
|
| 150 |
-
self.input = nn.Linear(2 * PARTICLES * DIMENSION, width) # Positions and velocities.
|
| 151 |
-
self.body = nn.Sequential(nn.Linear(width, width), nn.SiLU(),
|
| 152 |
-
nn.Linear(width, width), nn.SiLU())
|
| 153 |
-
self.head = nn.Linear(width, PARTICLES * (1 + DIMENSION)) # alpha and h per particle.
|
| 154 |
-
|
| 155 |
-
def forward(self, positions: torch.Tensor, velocities: torch.Tensor,
|
| 156 |
-
t: torch.Tensor) -> torch.Tensor:
|
| 157 |
-
"""positions, velocities: [B, n, d], t: [B] -> the bias force [B, n, d]."""
|
| 158 |
-
state = torch.cat([positions.flatten(1), velocities.flatten(1)], dim=1) # [B, 2nd].
|
| 159 |
-
hidden = self.body(self.input(state) + self.time_embedding(t)) # [B, width].
|
| 160 |
-
output = self.head(hidden).view(-1, PARTICLES, 1 + DIMENSION) # [B, n, 1 + d].
|
| 161 |
-
alpha = nn.functional.softplus(output[..., 0]) # [B, n] nonnegative, so b never points away.
|
| 162 |
-
return compose_bias(alpha, output[..., 1:], target_direction(positions))
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
# %% 06. Simulating the controlled dynamics
|
| 166 |
-
#
|
| 167 |
-
# The control enters the velocity equation divided by the mass, against the noise scale, so in units
|
| 168 |
-
# of the noise it is b / (m sigma) and Girsanov prices it at one half its squared size.
|
| 169 |
-
# Sections 7.27.1 and 7.27.2.
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
@torch.no_grad()
|
| 173 |
-
def roll_out(force: BiasForce | None, paths: int, steps: int, device: torch.device,
|
| 174 |
-
generator: torch.Generator) -> dict:
|
| 175 |
-
"""Run the Langevin dynamics with the given bias force, storing the states and the increments."""
|
| 176 |
-
dt = 1.0 / steps
|
| 177 |
-
positions = torch.full((paths, PARTICLES, DIMENSION), -1.0, device=device)
|
| 178 |
-
positions[:, :, 1] = 0.0 # Both particles start at the far well.
|
| 179 |
-
velocities = torch.randn(paths, PARTICLES, DIMENSION, generator=generator).to(device) * \
|
| 180 |
-
math.sqrt(TEMPERATURE / MASS) # Maxwell-Boltzmann velocities at the reference temperature.
|
| 181 |
-
stored_positions, stored_velocities, stored_u, increments = [], [], [], []
|
| 182 |
-
energy = torch.zeros(paths, device=device) # (1/2) int ||u||^2 dt along each path.
|
| 183 |
-
cross = torch.zeros(paths, device=device) # int u . dW along each path.
|
| 184 |
-
for step in range(steps):
|
| 185 |
-
t = torch.full((paths,), step * dt, device=device)
|
| 186 |
-
u = (force(positions, velocities, t) / (MASS * NOISE)) if force is not None else \
|
| 187 |
-
torch.zeros_like(positions) # The control in units of the noise.
|
| 188 |
-
noise = torch.randn(paths, PARTICLES, DIMENSION,
|
| 189 |
-
generator=generator).to(device) * math.sqrt(dt)
|
| 190 |
-
stored_positions.append(positions.clone())
|
| 191 |
-
stored_velocities.append(velocities.clone())
|
| 192 |
-
stored_u.append(u.clone())
|
| 193 |
-
increments.append(noise.clone())
|
| 194 |
-
energy = energy + 0.5 * (u ** 2).sum(dim=(1, 2)) * dt
|
| 195 |
-
cross = cross + (u * noise).sum(dim=(1, 2))
|
| 196 |
-
acceleration = physical_force(positions) / MASS - FRICTION * velocities # The reference.
|
| 197 |
-
velocities = velocities + (acceleration + NOISE * u) * dt + NOISE * noise
|
| 198 |
-
positions = positions + velocities * dt # The position follows the velocity.
|
| 199 |
-
return {
|
| 200 |
-
"positions": torch.stack(stored_positions), "velocities": torch.stack(stored_velocities),
|
| 201 |
-
"u_bar": torch.stack(stored_u), "noise": torch.stack(increments),
|
| 202 |
-
"reward": log_target(positions), "energy": energy, "cross": cross, "dt": dt,
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
def importance_weights(rollout: dict) -> torch.Tensor:
|
| 207 |
-
"""softmax over the batch of r(X_1) + log dR / dP^bbar, which cancels the unknown Z."""
|
| 208 |
-
log_weight = rollout["reward"] - rollout["cross"] - rollout["energy"]
|
| 209 |
-
return torch.softmax(log_weight, dim=0) # [P] normalized weights, summing to one.
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
def cross_entropy_loss(force: BiasForce, rollout: dict, weights: torch.Tensor) -> torch.Tensor:
|
| 213 |
-
"""The weighted average of -log dP^{b_theta} / dR along the stored paths, the loss of 7.27.3."""
|
| 214 |
-
steps, paths = rollout["u_bar"].shape[0], rollout["u_bar"].shape[1]
|
| 215 |
-
dt = rollout["dt"]
|
| 216 |
-
clock = torch.arange(steps, device=weights.device).to(torch.get_default_dtype()) * dt
|
| 217 |
-
times = clock[:, None].expand(steps, paths) # [T, P] the time of every stored state.
|
| 218 |
-
u = force(rollout["positions"].flatten(0, 1), rollout["velocities"].flatten(0, 1),
|
| 219 |
-
times.flatten()).view_as(rollout["u_bar"]) / (MASS * NOISE) # [T, P, n, d].
|
| 220 |
-
drift = (u * (rollout["u_bar"] * dt + rollout["noise"])).sum(dim=(0, 2, 3)) # [P].
|
| 221 |
-
energy = 0.5 * (u ** 2).sum(dim=(0, 2, 3)) * dt # [P] the control energy of each path.
|
| 222 |
-
return (weights * (energy - drift)).sum() # Weighted by the importance weights.
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
# %% 07. The report
|
| 226 |
-
def main() -> None:
|
| 227 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 228 |
-
parser.set_defaults(steps=100, lr=3e-3, width=64, samples=256)
|
| 229 |
-
args = parser.parse_args()
|
| 230 |
-
set_seed(args.seed)
|
| 231 |
-
device = pick_device(args.device)
|
| 232 |
-
loud = not args.quiet
|
| 233 |
-
generator = torch.Generator().manual_seed(args.seed)
|
| 234 |
-
|
| 235 |
-
linear = lambda t: 0.3 * t # The growth rate 0.3, so w^1_t rises at a constant rate.
|
| 236 |
-
late = lambda t: 0.3 * t ** 2 # The growth rate 0.6 t, which starts slowly.
|
| 237 |
-
near = {"linear": weighted_energy(2.0, lambda t: 1.0 - linear(t))
|
| 238 |
-
+ weighted_energy(-2.0, linear)}
|
| 239 |
-
far = {"linear": weighted_energy(2.0, lambda t: 1.0 - linear(t)) + weighted_energy(-4.0, linear),
|
| 240 |
-
"late": weighted_energy(2.0, lambda t: 1.0 - late(t)) + weighted_energy(-4.0, late)}
|
| 241 |
-
half = float(branch_weights(linear, torch.tensor(0.5)))
|
| 242 |
-
if loud:
|
| 243 |
-
print("Two branches from the point 0, ending at +2 with weight 0.7 and at -2 with 0.3")
|
| 244 |
-
print(f" weights at t = 1/2: {1 - half:.2f} and {half:.2f}; at t = 1: "
|
| 245 |
-
f"{1 - float(branch_weights(linear, torch.tensor(1.0))):.1f} and "
|
| 246 |
-
f"{float(branch_weights(linear, torch.tensor(1.0))):.1f}")
|
| 247 |
-
print(f" weighted energies {weighted_energy(2.0, lambda t: 1.0 - linear(t)):.1f} and "
|
| 248 |
-
f"{weighted_energy(-2.0, linear):.1f}, total {near['linear']:.1f}")
|
| 249 |
-
print(f" with the second target at -4: linear schedule {far['linear']:.1f}, "
|
| 250 |
-
f"late schedule {far['late']:.1f}, saving {far['linear'] - far['late']:.1f}")
|
| 251 |
-
|
| 252 |
-
bent = interpolant_energy(0.0, 2.0, 1.0)
|
| 253 |
-
straight = interpolant_energy(0.0, 2.0, 0.0)
|
| 254 |
-
grid = torch.tensor([0.0, 0.5, 1.0])
|
| 255 |
-
if loud:
|
| 256 |
-
print(f"\nThe interpolant with a correction of 1 passes through "
|
| 257 |
-
f"{[round(float(z), 2) for z in interpolant(0.0, 2.0, 1.0, grid)]}")
|
| 258 |
-
speed = [round(float(z), 0) for z in interpolant_velocity(0.0, 2.0, 1.0, grid)]
|
| 259 |
-
print(f" its velocity is {speed} and its energy {bent:.4f}, against "
|
| 260 |
-
f"{straight:.1f} for the straight line")
|
| 261 |
-
|
| 262 |
-
reference_mass, inside, outside = 0.1, 1.0, 0.01 # The two-outcome example of Section 7.27.1.
|
| 263 |
-
normalizer = reference_mass * inside + (1.0 - reference_mass) * outside
|
| 264 |
-
tilted_mass = reference_mass * inside / normalizer
|
| 265 |
-
reward_average = tilted_mass * math.log(inside) + (1.0 - tilted_mass) * math.log(outside)
|
| 266 |
-
tilt_entropy = reward_average - math.log(normalizer)
|
| 267 |
-
if loud:
|
| 268 |
-
print(f"\nA reference reaching B one time in ten, tilted by pi_B = 1 inside and 0.01 outside")
|
| 269 |
-
print(f" Z = {normalizer:.3f}, P*(B) = {tilted_mass:.3f}, "
|
| 270 |
-
f"a factor of {tilted_mass / reference_mass:.2f}")
|
| 271 |
-
print(f" E[r] = {reward_average:.3f}, -log Z = {-math.log(normalizer):.3f}, "
|
| 272 |
-
f"KL(P* || R) = {tilt_entropy:.3f}, J(b*) = {tilt_entropy - reward_average:.3f}")
|
| 273 |
-
|
| 274 |
-
step_drift = (-0.3 + 0.5) * 0.01 # The physical force plus the bias, over one step.
|
| 275 |
-
step_friction = -1.0 * 0.8 * 0.01 # Minus the friction times the current velocity.
|
| 276 |
-
step_noise = NOISE * math.sqrt(0.01) * 0.4 # The noise scale times a normal draw of 0.4.
|
| 277 |
-
unit_control = 0.5 / NOISE # The bias force measured in units of the noise.
|
| 278 |
-
plane = compose_bias(torch.tensor([[0.5]]), torch.tensor([[[0.3, -0.4]]]),
|
| 279 |
-
torch.tensor([[[1.0, 0.0]]]))[0, 0] # The example of Section 7.27.3.
|
| 280 |
-
log_weights = torch.tensor([-1.0, 0.0, 1.0])
|
| 281 |
-
softmax = torch.softmax(log_weights, dim=0)
|
| 282 |
-
if loud:
|
| 283 |
-
print(f"\nOne Euler-Maruyama step at dt = 0.01, with a noise scale of {NOISE:.4f}")
|
| 284 |
-
print(f" dv = {step_drift:.4f} {step_friction:+.4f} {step_noise:+.4f} = "
|
| 285 |
-
f"{step_drift + step_friction + step_noise:.4f}, dr = {0.8 * 0.01:.4f}")
|
| 286 |
-
print(f" the control in units of the noise is {unit_control:.4f}, contributing "
|
| 287 |
-
f"{0.5 * unit_control ** 2 * 0.01:.6f} per step and "
|
| 288 |
-
f"{0.5 * unit_control ** 2 * 0.01 * 100:.4f} over a hundred")
|
| 289 |
-
print(f" b = {[round(float(z), 1) for z in plane]}, with alpha "
|
| 290 |
-
f"{float((plane * torch.tensor([1.0, 0.0])).sum()):.1f} along s_hat "
|
| 291 |
-
f"and length {float(plane.norm()):.3f}")
|
| 292 |
-
print(f" three log-weights -1, 0, 1 normalize to {[round(float(z), 3) for z in softmax]}")
|
| 293 |
-
|
| 294 |
-
force = BiasForce(args.width).to(device)
|
| 295 |
-
reference_run = roll_out(None, args.samples, SIM_STEPS, device, generator)
|
| 296 |
-
before = roll_out(force, args.samples, SIM_STEPS, device, generator)
|
| 297 |
-
|
| 298 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 299 |
-
rollout = roll_out(force, PATHS, SIM_STEPS, device, generator) # Simulate under b_bar.
|
| 300 |
-
return cross_entropy_loss(force, rollout, importance_weights(rollout))
|
| 301 |
-
|
| 302 |
-
if loud:
|
| 303 |
-
print(f"\ntraining the bias force, {count_parameters(force):,} parameters, "
|
| 304 |
-
f"{PATHS} paths of {SIM_STEPS} steps per optimizer step")
|
| 305 |
-
losses = train(force, loss_fn, steps=args.steps, lr=args.lr, quiet=not loud, log_every=50)
|
| 306 |
-
after = roll_out(force, args.samples, SIM_STEPS, device, generator)
|
| 307 |
-
effective = float(1.0 / (importance_weights(after) ** 2).sum() / args.samples)
|
| 308 |
-
|
| 309 |
-
report = {
|
| 310 |
-
"weight of branch 1 at t = 1/2": half,
|
| 311 |
-
"weighted energy, branch 0": weighted_energy(2.0, lambda t: 1.0 - linear(t)),
|
| 312 |
-
"weighted energy, branch 1": weighted_energy(-2.0, linear),
|
| 313 |
-
"total, both ends at distance 2": near["linear"],
|
| 314 |
-
"total with the far target, linear": far["linear"],
|
| 315 |
-
"total with the far target, late": far["late"],
|
| 316 |
-
"a branch drawn at t = 1/2": branch_sample(half, generator),
|
| 317 |
-
"interpolant energy, correction 1": bent,
|
| 318 |
-
"interpolant energy, straight": straight,
|
| 319 |
-
"Z of the two outcomes": normalizer,
|
| 320 |
-
"P*(B)": tilted_mass,
|
| 321 |
-
"the factor the tilt applies": tilted_mass / reference_mass,
|
| 322 |
-
"E_{P*}[r]": reward_average,
|
| 323 |
-
"KL(P* || R)": tilt_entropy,
|
| 324 |
-
"-log Z, the value of J(b*)": -math.log(normalizer),
|
| 325 |
-
"one velocity step": step_drift + step_friction + step_noise,
|
| 326 |
-
"control in units of the noise": unit_control,
|
| 327 |
-
"control energy over a hundred steps": 0.5 * unit_control ** 2 * 0.01 * 100,
|
| 328 |
-
"the composed force": [round(float(z), 2) for z in plane],
|
| 329 |
-
"its length": float(plane.norm()),
|
| 330 |
-
"softmax of -1, 0, 1": [round(float(z), 3) for z in softmax],
|
| 331 |
-
"mean reward, reference": float(reference_run["reward"].mean()),
|
| 332 |
-
"mean reward, before training": float(before["reward"].mean()),
|
| 333 |
-
"mean reward, after training": float(after["reward"].mean()),
|
| 334 |
-
"control energy after training": float(after["energy"].mean()),
|
| 335 |
-
"effective sample size, fraction": effective,
|
| 336 |
-
"force written by hand against autograd": force_check(after["positions"][-1]),
|
| 337 |
-
"final loss": sum(losses[-10:]) / len(losses[-10:]),
|
| 338 |
-
}
|
| 339 |
-
print_report("Branched and Entangled bridges, Chapter 7 Sections 7.26 to 7.27.3", report)
|
| 340 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
if __name__ == "__main__":
|
| 344 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/csbm.py
DELETED
|
@@ -1,254 +0,0 @@
|
|
| 1 |
-
"""Categorical Schrodinger Bridge Matching: D-IMF on a path law small enough to write out.
|
| 2 |
-
|
| 3 |
-
With N intermediate times a path is N + 2 states and the whole path law is a finite table, so both
|
| 4 |
-
projections have one-line expressions. The reciprocal projection holds the endpoint coupling and
|
| 5 |
-
redraws the interior from the reference, the Markov projection keeps the one-step conditionals and
|
| 6 |
-
multiplies them into a chain, and the bridge is the only law in both sets. On two states and three
|
| 7 |
-
transitions the sixteen paths fit in a list, so the alternation runs exactly.
|
| 8 |
-
|
| 9 |
-
Chapter 7, Section 7.25 of the notes. Runs on S = 2, four time points, the uniform reference with
|
| 10 |
-
alpha = 0.35, the source law (0.5, 0.5) and the requested law (0.2, 0.8). Prints the uniform and
|
| 11 |
-
the ordered reference row, the exact bridge coupling, the conditionals of the first Markov
|
| 12 |
-
projection, the gap after each round, and how that gap moves with alpha and with the path length.
|
| 13 |
-
|
| 14 |
-
python lecture_7/csbm.py # 5 rounds of D-IMF, the five the chapter reports
|
| 15 |
-
python lecture_7/csbm.py --steps 1 # the single round the chapter writes out
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
from __future__ import annotations
|
| 20 |
-
|
| 21 |
-
import torch
|
| 22 |
-
|
| 23 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 24 |
-
|
| 25 |
-
DTYPE = torch.float64 # The gap reaches 1e-11 in five rounds, well below float32.
|
| 26 |
-
|
| 27 |
-
ALPHA = 0.35 # The probability of changing category in one step.
|
| 28 |
-
TRANSITIONS = 3 # N + 1, so four time points and sixteen paths.
|
| 29 |
-
PI_0 = torch.tensor([0.5, 0.5], dtype=DTYPE) # The source law.
|
| 30 |
-
PI_1 = torch.tensor([0.2, 0.8], dtype=DTYPE) # The requested law at the last time.
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
# %% 02. The reference over S categories
|
| 34 |
-
#
|
| 35 |
-
# The uniform reference holds the category with probability 1 - alpha and spreads the rest evenly
|
| 36 |
-
# over the alternatives. For categories that have an order a discretized Gaussian centred on the
|
| 37 |
-
# current one is available instead, under which a neighbour is likelier than a distant category.
|
| 38 |
-
# Section 7.25.
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
def uniform_reference(alpha: float, size: int) -> torch.Tensor:
|
| 42 |
-
"""A_n(x, y) = 1 - alpha on the diagonal and alpha / (S - 1) off it. [S, S]."""
|
| 43 |
-
off = alpha / (size - 1) # The share each alternative receives.
|
| 44 |
-
return torch.full((size, size), off, dtype=DTYPE) + torch.eye(size, dtype=DTYPE) * (
|
| 45 |
-
1.0 - alpha - off)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def ordered_reference(width: float, size: int) -> torch.Tensor:
|
| 49 |
-
"""Rows proportional to exp(-(y - x)^2 / 2 width^2), under which a nearby category is likelier."""
|
| 50 |
-
offset = torch.arange(size, dtype=DTYPE)[None, :] - torch.arange(size, dtype=DTYPE)[:, None]
|
| 51 |
-
weights = torch.exp(-offset ** 2 / (2.0 * width ** 2)) # [S, S] before normalizing.
|
| 52 |
-
return weights / weights.sum(dim=1, keepdim=True)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def sinkhorn(matrix: torch.Tensor, start: torch.Tensor, end: torch.Tensor, sweeps: int = 400
|
| 56 |
-
) -> torch.Tensor:
|
| 57 |
-
"""Scale diag(p_0) A^N to the two marginals, which gives the exact bridge coupling."""
|
| 58 |
-
f = torch.ones_like(start)
|
| 59 |
-
g = torch.ones_like(end)
|
| 60 |
-
for _ in range(sweeps):
|
| 61 |
-
g = end / (matrix.T @ f) # Column sums become exactly p_1.
|
| 62 |
-
f = start / (matrix @ g) # Row sums become exactly p_0.
|
| 63 |
-
return f[:, None] * matrix * g[None, :]
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
# %% 03. The two projections in discrete time
|
| 67 |
-
#
|
| 68 |
-
# A path law is a tensor with one axis per time. The reciprocal projection multiplies the endpoint
|
| 69 |
-
# coupling by the reference bridge weight of every path, and the Markov projection reads off the
|
| 70 |
-
# one-step conditionals and multiplies them back into a chain. Section 7.25.1.
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def reference_joint(matrix: torch.Tensor, steps: int) -> torch.Tensor:
|
| 74 |
-
"""The product of the transition probabilities along a path, as a tensor with one axis per time."""
|
| 75 |
-
joint = matrix.clone() # [x_0, x_1].
|
| 76 |
-
for _ in range(steps - 1):
|
| 77 |
-
joint = joint.unsqueeze(-1) * matrix # Append one more transition on a new axis.
|
| 78 |
-
return joint # [S] * (steps + 1).
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def bridge_table(matrix: torch.Tensor, steps: int) -> torch.Tensor:
|
| 82 |
-
"""R(x_interior | x_0, x_N), the reference bridge weight of every path given its two ends."""
|
| 83 |
-
whole = torch.linalg.matrix_power(matrix, steps) # [S, S] the reference over the interval.
|
| 84 |
-
size = len(matrix)
|
| 85 |
-
shape = [size] + [1] * (steps - 1) + [size] # Broadcast over the interior axes.
|
| 86 |
-
return reference_joint(matrix, steps) / whole.reshape(shape)
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def reciprocal_projection(coupling: torch.Tensor, matrix: torch.Tensor, steps: int) -> torch.Tensor:
|
| 90 |
-
"""proj_R(P) = P(x_0, x_N) R(x_in | x_0, x_N), the endpoint coupling with reference bridges."""
|
| 91 |
-
size = len(matrix)
|
| 92 |
-
shape = [size] + [1] * (steps - 1) + [size]
|
| 93 |
-
return coupling.reshape(shape) * bridge_table(matrix, steps)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def markov_projection(law: torch.Tensor) -> tuple[list[torch.Tensor], torch.Tensor]:
|
| 97 |
-
"""proj_M(P) = P(x_0) prod_n P(x_{t_n} | x_{t_{n-1}}), as the conditionals and the coupling."""
|
| 98 |
-
conditionals = []
|
| 99 |
-
for step in range(law.dim() - 1):
|
| 100 |
-
axes = [d for d in range(law.dim()) if d not in (step, step + 1)]
|
| 101 |
-
pair = law.sum(dim=axes) # [S, S] the joint of two consecutive states.
|
| 102 |
-
conditionals.append(pair / pair.sum(dim=1, keepdim=True))
|
| 103 |
-
joint = torch.diag(law.sum(dim=list(range(1, law.dim())))) # [S, S] the law at time 0.
|
| 104 |
-
for matrix in conditionals:
|
| 105 |
-
joint = joint @ matrix
|
| 106 |
-
return conditionals, joint
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
def transition_loss(true: torch.Tensor, network: torch.Tensor) -> float:
|
| 110 |
-
"""KL(P(x_t | x_s) || m_theta(x_t | x_s)) on one transition out of one state, in nats."""
|
| 111 |
-
return float((true * (true / network).log()).sum())
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
# %% 04. D-IMF on sixteen paths
|
| 115 |
-
#
|
| 116 |
-
# Alternating the two projections is iterative Markovian fitting with discrete time. The starting
|
| 117 |
-
# law is the independent coupling filled in with reference bridges, so both requested marginals are
|
| 118 |
-
# already exact and only the joint law over paths moves. Section 7.25.2.
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
def d_imf(coupling: torch.Tensor, matrix: torch.Tensor, steps: int, rounds: int,
|
| 122 |
-
target: torch.Tensor) -> tuple[list[float], list[torch.Tensor]]:
|
| 123 |
-
"""Run the alternation, returning the gap after each round and the first round's conditionals."""
|
| 124 |
-
gaps: list[float] = []
|
| 125 |
-
first: list[torch.Tensor] = []
|
| 126 |
-
for index in range(rounds):
|
| 127 |
-
law = reciprocal_projection(coupling, matrix, steps) # Reference bridges in the interior.
|
| 128 |
-
conditionals, coupling = markov_projection(law) # The chain and its endpoint coupling.
|
| 129 |
-
if index == 0:
|
| 130 |
-
first = conditionals
|
| 131 |
-
gaps.append(float((reciprocal_projection(coupling, matrix, steps) - target).abs().sum()))
|
| 132 |
-
return gaps, first
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def starting_gap(matrix: torch.Tensor, steps: int, start: torch.Tensor,
|
| 136 |
-
end: torch.Tensor) -> tuple[float, torch.Tensor, torch.Tensor]:
|
| 137 |
-
"""The gap of the independent coupling from the bridge, with both path laws."""
|
| 138 |
-
whole = torch.linalg.matrix_power(matrix, steps)
|
| 139 |
-
plan = sinkhorn(torch.diag(start) @ whole, start, end)
|
| 140 |
-
target = reciprocal_projection(plan, matrix, steps)
|
| 141 |
-
initial = reciprocal_projection(start[:, None] * end[None, :], matrix, steps)
|
| 142 |
-
return float((initial - target).abs().sum()), initial, target
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
# %% 05. The report
|
| 146 |
-
def main() -> None:
|
| 147 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 148 |
-
parser.set_defaults(steps=5) # --steps is the number of D-IMF rounds.
|
| 149 |
-
args = parser.parse_args()
|
| 150 |
-
set_seed(args.seed)
|
| 151 |
-
loud = not args.quiet
|
| 152 |
-
rounds = max(args.steps, 1)
|
| 153 |
-
|
| 154 |
-
A = uniform_reference(ALPHA, len(PI_0))
|
| 155 |
-
whole = torch.linalg.matrix_power(A, TRANSITIONS)
|
| 156 |
-
ordered = ordered_reference(1.0, 5) # Five ordered categories, a width of one category.
|
| 157 |
-
if loud:
|
| 158 |
-
print(f"S = 2, {TRANSITIONS + 1} time points, alpha = {ALPHA}, "
|
| 159 |
-
f"{len(PI_0) ** (TRANSITIONS + 1)} paths")
|
| 160 |
-
print(f" A = {[[round(float(z), 2) for z in row] for row in A]}, "
|
| 161 |
-
f"A^{TRANSITIONS} = {[[round(float(z), 4) for z in row] for row in whole]}")
|
| 162 |
-
print(f" the ordered reference out of the middle of five categories: "
|
| 163 |
-
f"{[round(float(z), 4) for z in ordered[2]]}")
|
| 164 |
-
|
| 165 |
-
plan = sinkhorn(torch.diag(PI_0) @ whole, PI_0, PI_1)
|
| 166 |
-
gap_0, initial, target = starting_gap(A, TRANSITIONS, PI_0, PI_1)
|
| 167 |
-
interior = [initial.sum(dim=[d for d in range(TRANSITIONS + 1) if d != k]) for k in (1, 2)]
|
| 168 |
-
independent = PI_0[:, None] * PI_1[None, :] # Where the alternation starts.
|
| 169 |
-
if loud:
|
| 170 |
-
print(f"\npi* = {[[round(float(z), 5) for z in row] for row in plan]}, against the "
|
| 171 |
-
f"independent coupling "
|
| 172 |
-
f"{[[round(float(z), 1) for z in row] for row in independent]}")
|
| 173 |
-
print(f" the path 1,1,1,1 has reference bridge weight "
|
| 174 |
-
f"{float(bridge_table(A, TRANSITIONS)[0, 0, 0, 0]):.4f}, so its probability is "
|
| 175 |
-
f"{float(initial[0, 0, 0, 0]):.4f} at the start and {float(target[0, 0, 0, 0]):.4f} "
|
| 176 |
-
f"under the bridge")
|
| 177 |
-
print(f" the path 2,2,2,2 has {float(initial[1, 1, 1, 1]):.4f} and "
|
| 178 |
-
f"{float(target[1, 1, 1, 1]):.4f}")
|
| 179 |
-
print(f" the two laws agree at the interior times, "
|
| 180 |
-
f"{[[round(float(z), 4) for z in m] for m in interior]}, and differ by "
|
| 181 |
-
f"{gap_0:.4f} over the paths")
|
| 182 |
-
|
| 183 |
-
gaps, conditionals = d_imf(independent, A, TRANSITIONS, rounds, target)
|
| 184 |
-
# Eight rounds reach machine precision, past which a ratio of two gaps measures rounding, so
|
| 185 |
-
# the report quotes the first one.
|
| 186 |
-
ratios = [gaps[k] / gaps[k + 1] for k in range(len(gaps) - 1) if gaps[k + 1] > 1e-14]
|
| 187 |
-
per_round = ratios[0] if ratios else gap_0 / gaps[0] # One round alone gives only the first.
|
| 188 |
-
network = torch.tensor([0.25, 0.75], dtype=DTYPE) # A network that is wrong by a little.
|
| 189 |
-
one_term = transition_loss(conditionals[-1][0], network)
|
| 190 |
-
if loud:
|
| 191 |
-
print(f"\nthe {len(conditionals)} conditionals of the first Markov projection")
|
| 192 |
-
for matrix in conditionals:
|
| 193 |
-
print(f" {[[round(float(z), 4) for z in row] for row in matrix]}")
|
| 194 |
-
print(f" gap {gap_0:.4f} at the start, then {['%.1e' % x for x in gaps]}")
|
| 195 |
-
print(f" ratios {[round(r, 1) for r in ratios]}")
|
| 196 |
-
print(f" the true conditional {[round(float(z), 4) for z in conditionals[-1][0]]} against "
|
| 197 |
-
f"a network at {[float(z) for z in network]} costs {one_term:.4f} nats")
|
| 198 |
-
|
| 199 |
-
# A reference that mixes well and a longer chain both leave the bridge less to do, which the
|
| 200 |
-
# starting gap records and the rate of convergence follows. Two rounds separate the two ratios
|
| 201 |
-
# from the float64 floor, which the sixty-four-path column reaches after three.
|
| 202 |
-
mixing = {}
|
| 203 |
-
for alpha in (0.2, ALPHA, 0.4):
|
| 204 |
-
matrix = uniform_reference(alpha, len(PI_0))
|
| 205 |
-
start, _, goal = starting_gap(matrix, TRANSITIONS, PI_0, PI_1)
|
| 206 |
-
later, _ = d_imf(independent, matrix, TRANSITIONS, 2, goal)
|
| 207 |
-
mixing[alpha] = (start, later[0] / later[1])
|
| 208 |
-
lengths = {}
|
| 209 |
-
for steps in (2, TRANSITIONS, 5):
|
| 210 |
-
start, _, goal = starting_gap(A, steps, PI_0, PI_1)
|
| 211 |
-
later, _ = d_imf(independent, A, steps, 2, goal)
|
| 212 |
-
lengths[steps] = (start, later[0] / later[1])
|
| 213 |
-
if loud:
|
| 214 |
-
print("\nalpha starting gap gap divided per round")
|
| 215 |
-
for alpha, (start, ratio) in mixing.items():
|
| 216 |
-
print(f" {alpha:<6g} {start:<14.5f} {ratio:.0f}")
|
| 217 |
-
print("\npaths starting gap gap divided per round")
|
| 218 |
-
for steps, (start, ratio) in lengths.items():
|
| 219 |
-
print(f" {len(PI_0) ** (steps + 1):<6d} {start:<14.5f} {ratio:.0f}")
|
| 220 |
-
|
| 221 |
-
report = {
|
| 222 |
-
"A^3": [round(float(z), 4) for z in whole.flatten()],
|
| 223 |
-
"the ordered reference row": [round(float(z), 4) for z in ordered[2]],
|
| 224 |
-
"pi*_11": float(plan[0, 0]),
|
| 225 |
-
"pi*_12": float(plan[0, 1]),
|
| 226 |
-
"reference bridge weight of 1,1,1,1": float(bridge_table(A, TRANSITIONS)[0, 0, 0, 0]),
|
| 227 |
-
"P^0(1,1,1,1)": float(initial[0, 0, 0, 0]),
|
| 228 |
-
"P*(1,1,1,1)": float(target[0, 0, 0, 0]),
|
| 229 |
-
"P^0(2,2,2,2)": float(initial[1, 1, 1, 1]),
|
| 230 |
-
"P*(2,2,2,2)": float(target[1, 1, 1, 1]),
|
| 231 |
-
"marginal at the first interior time": [round(float(z), 4) for z in interior[0]],
|
| 232 |
-
"marginal at the second": [round(float(z), 4) for z in interior[1]],
|
| 233 |
-
"gap at the start": gap_0,
|
| 234 |
-
"gap after one round": gaps[0],
|
| 235 |
-
"gap after the last round": gaps[-1],
|
| 236 |
-
"rounds run": rounds,
|
| 237 |
-
"gap divided per round": per_round,
|
| 238 |
-
"first conditional, out of state 1": [round(float(z), 4) for z in conditionals[0][0]],
|
| 239 |
-
"last conditional, out of state 1": [round(float(z), 4) for z in conditionals[-1][0]],
|
| 240 |
-
"one transition against a wrong network": one_term,
|
| 241 |
-
"starting gap at alpha = 0.2": mixing[0.2][0],
|
| 242 |
-
"rounds divide the gap by, at alpha = 0.2": mixing[0.2][1],
|
| 243 |
-
"rounds divide the gap by, at alpha = 0.4": mixing[0.4][1],
|
| 244 |
-
"starting gap on eight paths": lengths[2][0],
|
| 245 |
-
"starting gap on sixty-four paths": lengths[5][0],
|
| 246 |
-
"rounds divide the gap by, on eight paths": lengths[2][1],
|
| 247 |
-
"rounds divide the gap by, on sixty-four paths": lengths[5][1],
|
| 248 |
-
}
|
| 249 |
-
print_report("Categorical Schrodinger Bridge Matching, Chapter 7 Section 7.25", report)
|
| 250 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
if __name__ == "__main__":
|
| 254 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/ddsbm.py
DELETED
|
@@ -1,243 +0,0 @@
|
|
| 1 |
-
"""Discrete Diffusion Schrodinger Bridge Matching: one categorical feature of a molecular graph.
|
| 2 |
-
|
| 3 |
-
A molecule is a graph with a categorical feature on every atom and on every pair of atoms, and the
|
| 4 |
-
reference resamples each feature independently, so one feature with S categories specifies the
|
| 5 |
-
whole construction. Scaling the reference kernel to the two laws gives the bridge coupling for that
|
| 6 |
-
feature, and pinning a path at a known terminal graph gives the rate a network regresses onto.
|
| 7 |
-
Both stay usable on a state space of 10^27 graphs because the reference is a product over the
|
| 8 |
-
features, so every sum over graphs becomes a sum over the categories of one of them.
|
| 9 |
-
|
| 10 |
-
Chapter 7, Section 7.24 of the notes. Runs on one bond with three types, no bond, single and
|
| 11 |
-
double, at the survival probability r = 0.4. Prints the kernel and its generator, the bridge
|
| 12 |
-
coupling and the mass it leaves unchanged, the pinned rates and the posterior average a network
|
| 13 |
-
learns, and the counting over the forty-five features of a nine-atom graph.
|
| 14 |
-
|
| 15 |
-
python lecture_7/ddsbm.py # 200 Sinkhorn sweeps on the bond coupling
|
| 16 |
-
python lecture_7/ddsbm.py --steps 1 # the first sweep alone, computed by hand
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
# %% 01. Imports and configuration
|
| 20 |
-
from __future__ import annotations
|
| 21 |
-
|
| 22 |
-
import math
|
| 23 |
-
|
| 24 |
-
import torch
|
| 25 |
-
|
| 26 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 27 |
-
|
| 28 |
-
DTYPE = torch.float64 # The chapter quotes four figures of the coupling and of the pinned rates.
|
| 29 |
-
|
| 30 |
-
PRIOR = torch.full((3,), 1.0 / 3.0, dtype=DTYPE) # m, uniform over the three bond types.
|
| 31 |
-
BOND_0 = torch.tensor([0.7, 0.2, 0.1], dtype=DTYPE) # pi_0, mostly no bond.
|
| 32 |
-
BOND_1 = torch.tensor([0.1, 0.2, 0.7], dtype=DTYPE) # pi_1, mostly double.
|
| 33 |
-
SURVIVAL = 0.4 # r = alpha_bar(1) / alpha_bar(0) over the whole unit interval.
|
| 34 |
-
ATOMS = 9 # Heavy atoms in the molecule the chapter counts.
|
| 35 |
-
ATOM_TYPES, BOND_TYPES = 5, 4 # Categories of a node feature and of a bond feature.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
# %% 02. The reference process on one feature
|
| 39 |
-
#
|
| 40 |
-
# The kernel keeps the category with probability r and redraws it from the prior m otherwise. Over
|
| 41 |
-
# a short step r = 1 - h beta(t), so the rate out of x toward a different category y is the
|
| 42 |
-
# resampling rate beta(t) times the chance m(y) that the redraw lands on y. Section 7.24.1.
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def replacement_kernel(r: float, prior: torch.Tensor) -> torch.Tensor:
|
| 46 |
-
"""K_st(x, y) = r 1_{x=y} + (1 - r) m(y), the uniform-replacement kernel with a general prior."""
|
| 47 |
-
return r * torch.eye(len(prior), dtype=DTYPE) + (1.0 - r) * prior[None, :] # [S, S].
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def replacement_generator(beta: float, prior: torch.Tensor) -> torch.Tensor:
|
| 51 |
-
"""Q_t(x, y) = beta(t) m(y) off the diagonal, with the diagonal -beta(t) (1 - m(x))."""
|
| 52 |
-
rates = beta * prior[None, :].repeat(len(prior), 1) # [S, S].
|
| 53 |
-
rates = rates - torch.diag(rates.diagonal()) # Clear the diagonal before closing the row.
|
| 54 |
-
return rates - torch.diag(rates.sum(dim=1))
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def resampling_rate(r: float) -> float:
|
| 58 |
-
"""beta = -log alpha_bar(1) for the schedule alpha_bar(t) = r^t, which is constant in time."""
|
| 59 |
-
return -math.log(r)
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
# %% 03. A bridge for a single bond
|
| 63 |
-
#
|
| 64 |
-
# Scaling diag(pi_0) K_01 to the two marginals gives the coupling. The mass it leaves on the
|
| 65 |
-
# diagonal sits between the mass an independent coupling happens to leave and the largest any
|
| 66 |
-
# coupling can leave, and r fixes where between the two it lands. Section 7.24.2.
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
def sinkhorn(matrix: torch.Tensor, start: torch.Tensor, end: torch.Tensor, sweeps: int
|
| 70 |
-
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, list[float]]:
|
| 71 |
-
"""Scale a positive matrix to the two marginals, returning f, g, the plan and the column errors."""
|
| 72 |
-
f = torch.ones_like(start)
|
| 73 |
-
g = torch.ones_like(end)
|
| 74 |
-
errors: list[float] = []
|
| 75 |
-
for _ in range(sweeps):
|
| 76 |
-
g = end / (matrix.T @ f) # Column sums become exactly pi_1.
|
| 77 |
-
f = start / (matrix @ g) # Row sums become exactly pi_0, and the columns move again.
|
| 78 |
-
errors.append(float(((f[:, None] * matrix * g[None, :]).sum(dim=0) - end).abs().sum()))
|
| 79 |
-
return f, g, f[:, None] * matrix * g[None, :], errors
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def held_mass(plan: torch.Tensor) -> float:
|
| 83 |
-
"""The trace of the plan, the fraction of bonds whose type the bridge does not change."""
|
| 84 |
-
return float(plan.diagonal().sum())
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
# %% 04. Training with the terminal graph fixed
|
| 88 |
-
#
|
| 89 |
-
# Pinning the endpoint is an h-transform with the reference probability of arriving at x_1 in the
|
| 90 |
-
# place of the potential, so a move onto the terminal type has its rate multiplied and a move off
|
| 91 |
-
# it divided. Several terminal types can occur at one state, and the minimizer of the rate error
|
| 92 |
-
# over a single number is their posterior average. Section 7.24.3.
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def pinned_rate(generator: torch.Tensor, kernel: torch.Tensor, x: int, y: int, x1: int) -> float:
|
| 96 |
-
"""Q_t(x, y; x_1) = Q_t(x, y) K_t1(y, x_1) / K_t1(x, x_1), the rate of the bridge ending at x_1."""
|
| 97 |
-
return float(generator[x, y] * kernel[y, x1] / kernel[x, x1])
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def half_survival_time(r: float) -> float:
|
| 101 |
-
"""The time where alpha_bar(1) / alpha_bar(t) = r^{1-t} equals 1/2, on alpha_bar(t) = r^t."""
|
| 102 |
-
return 1.0 - math.log(0.5) / math.log(r) # At r = 0.4 this is log 0.8 / log 0.4 = 0.2435.
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def rate_error(controlled: float, network: float) -> float:
|
| 106 |
-
"""Q^pin log(Q^pin / Q^theta) - Q^pin + Q^theta, the Bregman divergence of u log u on one edge."""
|
| 107 |
-
return controlled * math.log(controlled / network) - controlled + network
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
def posterior_loss(rates: list[float], weights: list[float], network: float) -> float:
|
| 111 |
-
"""The expected rate error at a state whose terminal type is uncertain, over the posterior."""
|
| 112 |
-
return sum(w * rate_error(q, network) for q, w in zip(rates, weights))
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
# %% 05. The factorization over a graph
|
| 116 |
-
#
|
| 117 |
-
# The reference applies the same kernel independently at every node and every pair of nodes, so the
|
| 118 |
-
# generator changes one feature at a time, the total jump rate out of a graph is the sum of the
|
| 119 |
-
# per-feature rates, and in the pinned rate every factor cancels but the one of the feature that
|
| 120 |
-
# moves. Section 7.24.4.
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
def feature_count(atoms: int) -> tuple[int, int, int]:
|
| 124 |
-
"""The node features, the bond features and their total, for a molecule with this many atoms."""
|
| 125 |
-
bonds = atoms * (atoms - 1) // 2 # One feature per unordered pair.
|
| 126 |
-
return atoms, bonds, atoms + bonds
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
def state_space_size(atoms: int, atom_types: int, bond_types: int) -> float:
|
| 130 |
-
"""atom_types^nodes times bond_types^bonds, the number of graphs the sums would run over."""
|
| 131 |
-
nodes, bonds, _ = feature_count(atoms)
|
| 132 |
-
return float(atom_types ** nodes) * float(bond_types ** bonds)
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
def total_jump_rate(features: int, beta: float, categories: int) -> float:
|
| 136 |
-
"""The sum of the per-feature rates out of a graph, which fixes the step size of the sampler."""
|
| 137 |
-
return features * beta * (1.0 - 1.0 / categories) # A redraw lands elsewhere with 1 - m(x).
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
# %% 06. The report
|
| 141 |
-
def main() -> None:
|
| 142 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 143 |
-
parser.set_defaults(steps=200) # --steps is the number of Sinkhorn sweeps on the bond coupling.
|
| 144 |
-
args = parser.parse_args()
|
| 145 |
-
set_seed(args.seed)
|
| 146 |
-
loud = not args.quiet
|
| 147 |
-
sweeps = max(args.steps, 1)
|
| 148 |
-
|
| 149 |
-
kernel = replacement_kernel(SURVIVAL, PRIOR)
|
| 150 |
-
beta = resampling_rate(SURVIVAL)
|
| 151 |
-
rates = replacement_generator(beta, PRIOR)
|
| 152 |
-
reference = torch.diag(BOND_0) @ kernel
|
| 153 |
-
if loud:
|
| 154 |
-
print(f"One bond with three types at r = {SURVIVAL}, beta = {beta:.4f}, "
|
| 155 |
-
f"off-diagonal rate {float(rates[0, 1]):.4f}")
|
| 156 |
-
print(f" K_01 = {[[round(float(z), 1) for z in row] for row in kernel]}")
|
| 157 |
-
print(f" diag(pi_0) K_01 has column sums "
|
| 158 |
-
f"{[round(float(z), 2) for z in reference.sum(dim=0)]}, so the first g is "
|
| 159 |
-
f"{[round(float(z), 4) for z in BOND_1 / reference.sum(dim=0)]}")
|
| 160 |
-
|
| 161 |
-
f, g, plan, errors = sinkhorn(reference, BOND_0, BOND_1, sweeps)
|
| 162 |
-
independent = float((BOND_0 * BOND_1).sum())
|
| 163 |
-
largest = float(torch.minimum(BOND_0, BOND_1).sum())
|
| 164 |
-
held = {r: held_mass(sinkhorn(torch.diag(BOND_0) @ replacement_kernel(r, PRIOR),
|
| 165 |
-
BOND_0, BOND_1, 200)[2])
|
| 166 |
-
for r in (0.2, 0.4, 0.6, 0.8, 0.99)}
|
| 167 |
-
if loud:
|
| 168 |
-
print(f"\nAfter {sweeps} sweeps, column error {errors[-1]:.2e}")
|
| 169 |
-
print(f" f = {[round(float(z), 4) for z in f]}, "
|
| 170 |
-
f"g = {[round(float(z), 4) for z in g]}")
|
| 171 |
-
print(f" pi* = {[[round(float(z), 3) for z in row] for row in plan]}")
|
| 172 |
-
print(f" mass that stays: bridge {held_mass(plan):.4f}, independent {independent:.2f}, "
|
| 173 |
-
f"largest possible {largest:.1f}")
|
| 174 |
-
print(f" diagonal mass at r = 0.2, 0.4, 0.6, 0.8, 0.99: "
|
| 175 |
-
f"{[round(held[r], 4) for r in (0.2, 0.4, 0.6, 0.8, 0.99)]}")
|
| 176 |
-
|
| 177 |
-
pinned_time = half_survival_time(SURVIVAL)
|
| 178 |
-
pinned_kernel = replacement_kernel(0.5, PRIOR)
|
| 179 |
-
toward = pinned_rate(rates, pinned_kernel, 0, 2, 2) # A move onto the terminal type.
|
| 180 |
-
away = pinned_rate(rates, pinned_kernel, 2, 0, 2) # A move off it.
|
| 181 |
-
between = pinned_rate(rates, pinned_kernel, 0, 1, 2) # Between two non-terminal types.
|
| 182 |
-
one_edge = rate_error(toward, 1.0) # The term one edge contributes at a network rate of 1.
|
| 183 |
-
weights = [0.6, 0.4] # The posterior over the two terminal types at this state.
|
| 184 |
-
posterior = weights[0] * toward + weights[1] * between
|
| 185 |
-
losses = {q: posterior_loss([toward, between], weights, q) for q in (posterior, 1.0, 0.5)}
|
| 186 |
-
if loud:
|
| 187 |
-
print(f"\nAt t = {pinned_time:.4f} the survival ratio to time 1 is 1/2, so K_t1 is "
|
| 188 |
-
f"{float(pinned_kernel[0, 0]):.4f} on the diagonal and "
|
| 189 |
-
f"{float(pinned_kernel[0, 1]):.4f} off it")
|
| 190 |
-
print(f" pinned rates: {toward:.4f} toward the terminal type, {away:.4f} away from it "
|
| 191 |
-
f"and {between:.4f} between two others, a factor of {toward / away:.0f}")
|
| 192 |
-
print(f" one edge at a network rate of 1 contributes {one_edge:.4f}")
|
| 193 |
-
print(f" with two terminal types the network learns {posterior:.4f}, at a loss of "
|
| 194 |
-
f"{losses[posterior]:.4f}, against {losses[1.0]:.4f} at 1 and {losses[0.5]:.4f} at 0.5")
|
| 195 |
-
|
| 196 |
-
nodes, bonds, features = feature_count(ATOMS)
|
| 197 |
-
graphs = state_space_size(ATOMS, ATOM_TYPES, BOND_TYPES)
|
| 198 |
-
jump_rate = total_jump_rate(bonds, beta, BOND_TYPES)
|
| 199 |
-
if loud:
|
| 200 |
-
print(f"\n{ATOMS} heavy atoms: {nodes} node features and {bonds} bond features, "
|
| 201 |
-
f"{features} in all")
|
| 202 |
-
print(f" {ATOM_TYPES}^{nodes} x {BOND_TYPES}^{bonds} = {graphs:.1e} graphs, "
|
| 203 |
-
f"which no sum reaches")
|
| 204 |
-
print(f" the total jump rate out of a graph is {bonds} x {beta:.4f} x "
|
| 205 |
-
f"(1 - 1/{BOND_TYPES}) = {jump_rate:.2f} per unit time")
|
| 206 |
-
|
| 207 |
-
report = {
|
| 208 |
-
"beta of alpha_bar(t) = 0.4^t": beta,
|
| 209 |
-
"off-diagonal reference rate": float(rates[0, 1]),
|
| 210 |
-
"K_01 on the diagonal": float(kernel[0, 0]),
|
| 211 |
-
"K_01 off the diagonal": float(kernel[0, 1]),
|
| 212 |
-
"rows of the generator sum to": float(rates.sum(dim=1).abs().max()),
|
| 213 |
-
"column sums of diag(pi_0) K_01": [round(float(z), 2) for z in reference.sum(dim=0)],
|
| 214 |
-
"the first g": [round(float(z), 4) for z in BOND_1 / reference.sum(dim=0)],
|
| 215 |
-
"sweeps run": sweeps,
|
| 216 |
-
"column error after the last sweep": errors[-1],
|
| 217 |
-
"f": [round(float(z), 4) for z in f],
|
| 218 |
-
"g": [round(float(z), 4) for z in g],
|
| 219 |
-
"bond plan, no bond to double": float(plan[0, 2]),
|
| 220 |
-
"bond mass that stays": held_mass(plan),
|
| 221 |
-
"bond mass, independent coupling": independent,
|
| 222 |
-
"bond mass, largest possible": largest,
|
| 223 |
-
"diagonal mass at r = 0.2": held[0.2],
|
| 224 |
-
"diagonal mass at r = 0.99": held[0.99],
|
| 225 |
-
"time where the survival ratio is 1/2": pinned_time,
|
| 226 |
-
"pinned rate toward the terminal": toward,
|
| 227 |
-
"pinned rate away from it": away,
|
| 228 |
-
"pinned rate between two others": between,
|
| 229 |
-
"one edge at a network rate of 1": one_edge,
|
| 230 |
-
"posterior average of the rates": posterior,
|
| 231 |
-
"loss at the posterior average": losses[posterior],
|
| 232 |
-
"loss at 1": losses[1.0],
|
| 233 |
-
"loss at 0.5": losses[0.5],
|
| 234 |
-
"node, bond and total features": [nodes, bonds, features],
|
| 235 |
-
"graphs on nine heavy atoms": f"{graphs:.1e}",
|
| 236 |
-
"total jump rate out of a graph": jump_rate,
|
| 237 |
-
}
|
| 238 |
-
print_report("Discrete Diffusion Schrodinger Bridge Matching, Chapter 7 Section 7.24", report)
|
| 239 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
if __name__ == "__main__":
|
| 243 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/discrete_bridge.py
DELETED
|
@@ -1,267 +0,0 @@
|
|
| 1 |
-
"""The Schrodinger bridge over a finite set: rates in place of drifts.
|
| 2 |
-
|
| 3 |
-
On a finite state space there is no small displacement, so the control is a reweighting of jump
|
| 4 |
-
rates and relative entropy on paths is a time integral of a Bregman divergence between two rate
|
| 5 |
-
matrices. Everything else carries over. The chain rule splits the problem into an endpoint term and
|
| 6 |
-
a bridge term, the two potentials are the Sinkhorn scalings of the reference endpoint law, and
|
| 7 |
-
under Doob's h-transform each reference rate is multiplied by the ratio of a backward potential
|
| 8 |
-
at the destination to its value at the current state.
|
| 9 |
-
|
| 10 |
-
Chapter 7, Sections 7.21 to 7.23.3 of the notes. Runs the two-state continuous-time example with
|
| 11 |
-
the generator Q, the source law (0.5, 0.5) and the requested terminal law (0.2, 0.8). Prints the
|
| 12 |
-
bridge rates at three times, the reverse rates, the Euler sampler's error as the step shrinks,
|
| 13 |
-
and the cost computed on the endpoints and on the paths.
|
| 14 |
-
|
| 15 |
-
python lecture_7/discrete_bridge.py # 10 Euler steps, then 20, 40 and 100
|
| 16 |
-
python lecture_7/discrete_bridge.py --steps 20 # a finer sampler, for a first look
|
| 17 |
-
"""
|
| 18 |
-
|
| 19 |
-
# %% 01. Imports and configuration
|
| 20 |
-
from __future__ import annotations
|
| 21 |
-
|
| 22 |
-
import math
|
| 23 |
-
|
| 24 |
-
import torch
|
| 25 |
-
|
| 26 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 27 |
-
|
| 28 |
-
DTYPE = torch.float64 # Matrix exponentials and a time integral, both in double precision.
|
| 29 |
-
|
| 30 |
-
Q = torch.tensor([[-1.0, 1.0], [1.5, -1.5]], dtype=DTYPE) # The reference generator.
|
| 31 |
-
PI_0 = torch.tensor([0.5, 0.5], dtype=DTYPE) # The source law.
|
| 32 |
-
PI_1 = torch.tensor([0.2, 0.8], dtype=DTYPE) # The requested terminal law.
|
| 33 |
-
STATIONARY = torch.tensor([0.6, 0.4], dtype=DTYPE) # Proportional to (1.5, 1), the rows of Q.
|
| 34 |
-
GRID = 1001 # Points in the Simpson rule for the time integral of the rate error.
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
# %% 02. Relative entropy between two jump processes
|
| 38 |
-
#
|
| 39 |
-
# Collecting the one-step relative entropies and letting the step go to zero leaves a time integral
|
| 40 |
-
# of the Bregman divergence of u log u between the two rates, which is nonnegative and vanishes
|
| 41 |
-
# only when they agree. The divergence is not symmetric in the ratio. Section 7.21.1.
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def rate_error(controlled: torch.Tensor, reference: torch.Tensor) -> torch.Tensor:
|
| 45 |
-
"""Q^theta log(Q^theta / Q) - Q^theta + Q, the Bregman divergence of u log u, entry by entry."""
|
| 46 |
-
safe = controlled.clamp(min=1e-300) # A zero rate contributes the reference rate alone.
|
| 47 |
-
return controlled * (safe / reference).log() - controlled + reference
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def scaling_cost(c: float) -> float:
|
| 51 |
-
"""c log c - c + 1, the cost per unit of reference rate of scaling every rate by c."""
|
| 52 |
-
return c * math.log(c) - c + 1.0
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
# %% 03. The Schrodinger problem for a chain
|
| 56 |
-
#
|
| 57 |
-
# The endpoint law of a chain with a time-independent generator is diag(pi_0^ref) e^Q, and the two
|
| 58 |
-
# potentials are the Sinkhorn scalings of that matrix to the two marginals. Section 7.21.2.
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
def endpoint_law(generator: torch.Tensor, start: torch.Tensor) -> torch.Tensor:
|
| 62 |
-
"""R_01 = diag(pi_0) e^Q, the reference probability of starting at x and ending at y."""
|
| 63 |
-
return torch.diag(start) @ torch.matrix_exp(generator) # [S, S].
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def sinkhorn(matrix: torch.Tensor, start: torch.Tensor, end: torch.Tensor, sweeps: int = 400
|
| 67 |
-
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 68 |
-
"""Scale a positive matrix to the two marginals, returning f, g and the plan f R_01 g."""
|
| 69 |
-
f = torch.ones_like(start)
|
| 70 |
-
g = torch.ones_like(end)
|
| 71 |
-
for _ in range(sweeps):
|
| 72 |
-
g = end / (matrix.T @ f) # Column sums become exactly pi_1.
|
| 73 |
-
f = start / (matrix @ g) # Row sums become exactly pi_0.
|
| 74 |
-
return f, g, f[:, None] * matrix * g[None, :]
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def relative_entropy(p: torch.Tensor, q: torch.Tensor) -> float:
|
| 78 |
-
"""Sum over entries of p log(p / q), for two positive matrices or vectors."""
|
| 79 |
-
mask = p > 0
|
| 80 |
-
return float((p[mask] * (p[mask] / q[mask]).log()).sum())
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
# %% 04. Rates reweighted by an h-transform
|
| 84 |
-
#
|
| 85 |
-
# The backward potential is g averaged over the reference from the current time to time 1, and each
|
| 86 |
-
# off-diagonal rate is multiplied by the ratio of that potential at the destination to its value at
|
| 87 |
-
# the current state. The diagonal is then set so that the row sums to zero. Section 7.22.
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def backward_potential(generator: torch.Tensor, g: torch.Tensor, t: float) -> torch.Tensor:
|
| 91 |
-
"""phi_t = e^{Q(1-t)} g, the reference average of g at the final state seen from time t."""
|
| 92 |
-
return torch.matrix_exp(generator * (1.0 - t)) @ g # [S].
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def tilted_generator(generator: torch.Tensor, phi: torch.Tensor) -> torch.Tensor:
|
| 96 |
-
"""Q*(x, y) = Q(x, y) phi(y) / phi(x) off the diagonal, with the diagonal closing each row."""
|
| 97 |
-
tilted = generator * phi[None, :] / phi[:, None] # [S, S] every entry reweighted.
|
| 98 |
-
tilted.fill_diagonal_(0.0) # Discard the reweighted diagonal, which has no meaning.
|
| 99 |
-
return tilted - torch.diag(tilted.sum(dim=1)) # Each row now sums to zero.
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def bridge_marginal(generator: torch.Tensor, start: torch.Tensor, f: torch.Tensor,
|
| 103 |
-
g: torch.Tensor, t: float) -> torch.Tensor:
|
| 104 |
-
"""p*_t = psi_t phi_t, the forward potential carried under the reference times the backward one."""
|
| 105 |
-
psi = (start * f) @ torch.matrix_exp(generator * t) # [S] the forward factor.
|
| 106 |
-
return psi * backward_potential(generator, g, t) # [S] their entrywise product.
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
# %% 05. Sampling the bridge with Euler steps
|
| 110 |
-
#
|
| 111 |
-
# Over a step the chain moves to a different state with probability dt Q*(x, y) and stays with the
|
| 112 |
-
# remaining probability, which is the Euler sampler of Chapter 4 applied to a generator that
|
| 113 |
-
# changes with time. Section 7.23.1.
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
def euler_marginal(generator: torch.Tensor, start: torch.Tensor, f: torch.Tensor,
|
| 117 |
-
g: torch.Tensor, steps: int) -> tuple[torch.Tensor, list[float]]:
|
| 118 |
-
"""Propagate p <- p (I + dt Q*_t) and record the probability of leaving state 1 at each step."""
|
| 119 |
-
dt = 1.0 / steps
|
| 120 |
-
p = start.clone()
|
| 121 |
-
leaving = []
|
| 122 |
-
identity = torch.eye(len(start), dtype=DTYPE)
|
| 123 |
-
for step in range(steps):
|
| 124 |
-
tilted = tilted_generator(generator, backward_potential(generator, g, step * dt))
|
| 125 |
-
leaving.append(float(tilted[0, 1] * dt)) # The jump probability out of state 1.
|
| 126 |
-
p = p @ (identity + dt * tilted) # One Euler step of the master equation.
|
| 127 |
-
return p, leaving
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
# %% 06. The same bridge run backwards
|
| 131 |
-
#
|
| 132 |
-
# Matching the probability flux on an edge in both directions fixes the reverse rate, which is the
|
| 133 |
-
# forward rate times the ratio of the marginal at the two ends of the edge. Section 7.23.2.
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
def reverse_rates(tilted: torch.Tensor, marginal: torch.Tensor) -> torch.Tensor:
|
| 137 |
-
"""Q_hat(y, x) = Q(x, y) p_t(x) / p_t(y), the rates under which the flux on an edge matches."""
|
| 138 |
-
return (tilted * marginal[:, None] / marginal[None, :]).T # [S, S], reversed in both indices.
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
# %% 07. The cost of the bridge, computed twice
|
| 142 |
-
#
|
| 143 |
-
# On the endpoints the cost is the relative entropy of the plan from the reference endpoint law. On
|
| 144 |
-
# the paths it is the time integral of the rate error, weighted by the marginal of the bridge at
|
| 145 |
-
# that time. The two agree. Section 7.23.3.
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
def path_cost(generator: torch.Tensor, start: torch.Tensor, f: torch.Tensor, g: torch.Tensor,
|
| 149 |
-
points: int = GRID) -> tuple[float, float]:
|
| 150 |
-
"""Integrate the weighted rate error over [0, 1] by Simpson's rule.
|
| 151 |
-
|
| 152 |
-
Returns the integral and the value of the integrand at t = 1/2, the one the chapter prints.
|
| 153 |
-
"""
|
| 154 |
-
off = ~torch.eye(len(generator), dtype=torch.bool) # [S, S] true away from the diagonal.
|
| 155 |
-
times = torch.linspace(0.0, 1.0, points, dtype=DTYPE)
|
| 156 |
-
values = []
|
| 157 |
-
for t in times.tolist():
|
| 158 |
-
tilted = tilted_generator(generator, backward_potential(generator, g, t))
|
| 159 |
-
marginal = bridge_marginal(generator, start, f, g, t)
|
| 160 |
-
error = torch.zeros_like(tilted)
|
| 161 |
-
error[off] = rate_error(tilted[off], generator[off]) # Only the edges carry a cost.
|
| 162 |
-
values.append(float((marginal * error.sum(dim=1)).sum()))
|
| 163 |
-
weights = torch.ones(points, dtype=DTYPE)
|
| 164 |
-
weights[1:-1:2] = 4.0 # Simpson's rule on an even number of intervals.
|
| 165 |
-
weights[2:-1:2] = 2.0
|
| 166 |
-
integral = float((weights * torch.tensor(values, dtype=DTYPE)).sum() / (3.0 * (points - 1)))
|
| 167 |
-
return integral, values[(points - 1) // 2]
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
# %% 08. The report
|
| 171 |
-
def main() -> None:
|
| 172 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 173 |
-
parser.set_defaults(steps=10) # --steps is the coarsest number of Euler steps in the table.
|
| 174 |
-
args = parser.parse_args()
|
| 175 |
-
set_seed(args.seed)
|
| 176 |
-
loud = not args.quiet
|
| 177 |
-
coarse = max(args.steps, 2)
|
| 178 |
-
|
| 179 |
-
if loud:
|
| 180 |
-
print("Scaling every reference rate by c costs c log c - c + 1 per unit of rate")
|
| 181 |
-
print(f" halving {scaling_cost(0.5):.4f}, doubling {scaling_cost(2.0):.4f}")
|
| 182 |
-
|
| 183 |
-
R01 = endpoint_law(Q, PI_0)
|
| 184 |
-
f, g, plan = sinkhorn(R01, PI_0, PI_1)
|
| 185 |
-
phi_half = backward_potential(Q, g, 0.5)
|
| 186 |
-
tilted_half = tilted_generator(Q, phi_half)
|
| 187 |
-
tilted_zero = tilted_generator(Q, backward_potential(Q, g, 0.0))
|
| 188 |
-
tilted_one = tilted_generator(Q, g)
|
| 189 |
-
marginal_half = bridge_marginal(Q, PI_0, f, g, 0.5)
|
| 190 |
-
marginal_one = bridge_marginal(Q, PI_0, f, g, 1.0)
|
| 191 |
-
reverse = reverse_rates(tilted_half, marginal_half)
|
| 192 |
-
if loud:
|
| 193 |
-
print(f"\ne^Q = {[[round(float(z), 4) for z in row] for row in torch.matrix_exp(Q)]}")
|
| 194 |
-
print(f"R_01 = {[[round(float(z), 4) for z in row] for row in R01]}, "
|
| 195 |
-
f"column sums {[round(float(z), 4) for z in R01.sum(0)]}")
|
| 196 |
-
print(f" f = {[round(float(z), 4) for z in f]}, g = {[round(float(z), 4) for z in g]}, "
|
| 197 |
-
f"g ratio {float(g[1] / g[0]):.3f}")
|
| 198 |
-
print(f" pi* = {[[round(float(z), 4) for z in row] for row in plan]}")
|
| 199 |
-
print(f" phi_1/2 = {[round(float(z), 4) for z in phi_half]}, "
|
| 200 |
-
f"ratio {float(phi_half[1] / phi_half[0]):.2f}")
|
| 201 |
-
print(f" Q*_1/2 = {[[round(float(z), 4) for z in row] for row in tilted_half]}")
|
| 202 |
-
print(f" rates out of the two states: at t = 0 "
|
| 203 |
-
f"{float(tilted_zero[0, 1]):.4f} and {float(tilted_zero[1, 0]):.4f}, "
|
| 204 |
-
f"at t = 1 {float(tilted_one[0, 1]):.4f} and {float(tilted_one[1, 0]):.4f}")
|
| 205 |
-
print(f" marginal at t = 1/2 {[round(float(z), 4) for z in marginal_half]}, "
|
| 206 |
-
f"at t = 1 {[round(float(z), 4) for z in marginal_one]}")
|
| 207 |
-
print(f" reverse rates at t = 1/2: {float(reverse[1, 0]):.4f} from 2 back to 1, "
|
| 208 |
-
f"{float(reverse[0, 1]):.4f} from 1 back to 2")
|
| 209 |
-
print(f" the flux on the edge: {float(marginal_half[0] * tilted_half[0, 1]):.4f} "
|
| 210 |
-
f"against {float(marginal_half[1] * reverse[1, 0]):.4f}")
|
| 211 |
-
|
| 212 |
-
sampler = {}
|
| 213 |
-
for steps in (coarse, 2 * coarse, 4 * coarse, 10 * coarse):
|
| 214 |
-
terminal, leaving = euler_marginal(Q, PI_0, f, g, steps)
|
| 215 |
-
sampler[steps] = (terminal, float((terminal - PI_1).abs().sum() / 2), leaving)
|
| 216 |
-
if loud:
|
| 217 |
-
print("\nsteps terminal law error first and last jump probability")
|
| 218 |
-
for steps, (terminal, error, leaving) in sampler.items():
|
| 219 |
-
print(f" {steps:<5d} ({float(terminal[0]):.4f}, {float(terminal[1]):.4f})"
|
| 220 |
-
f" {error:.4f} {leaving[0]:.4f}, {leaving[-1]:.4f}")
|
| 221 |
-
|
| 222 |
-
endpoint_cost = relative_entropy(plan, R01)
|
| 223 |
-
integral, at_half = path_cost(Q, PI_0, f, g)
|
| 224 |
-
speeds = {}
|
| 225 |
-
for lam in (0.5, 1.0, 2.0, 4.0):
|
| 226 |
-
scaled_law = endpoint_law(lam * Q, PI_0)
|
| 227 |
-
_, _, scaled_plan = sinkhorn(scaled_law, PI_0, PI_1)
|
| 228 |
-
speeds[lam] = relative_entropy(scaled_plan, scaled_law)
|
| 229 |
-
limit = relative_entropy(PI_1, STATIONARY)
|
| 230 |
-
if loud:
|
| 231 |
-
print(f"\nKL(pi* || R_01) = {endpoint_cost:.4f}")
|
| 232 |
-
print(f" the integrand at t = 1/2 is {at_half:.4f}, and its integral is {integral:.4f}")
|
| 233 |
-
print(f" at lambda = 1/2, 1, 2, 4 the cost is "
|
| 234 |
-
f"{[round(speeds[k], 4) for k in (0.5, 1.0, 2.0, 4.0)]}")
|
| 235 |
-
print(f" the limit KL(pi_1 || (0.6, 0.4)) is {limit:.4f}")
|
| 236 |
-
|
| 237 |
-
report = {
|
| 238 |
-
"cost of halving every rate": scaling_cost(0.5),
|
| 239 |
-
"cost of doubling every rate": scaling_cost(2.0),
|
| 240 |
-
"R_01 column sums": [round(float(z), 4) for z in R01.sum(dim=0)],
|
| 241 |
-
"f": [round(float(z), 4) for z in f],
|
| 242 |
-
"g": [round(float(z), 4) for z in g],
|
| 243 |
-
"pi*_11": float(plan[0, 0]),
|
| 244 |
-
"phi at t = 1/2": [round(float(z), 4) for z in phi_half],
|
| 245 |
-
"bridge rate out of state 1": float(tilted_half[0, 1]),
|
| 246 |
-
"bridge rate out of state 2": float(tilted_half[1, 0]),
|
| 247 |
-
"rate out of state 1 at t = 1": float(tilted_one[0, 1]),
|
| 248 |
-
"marginal at t = 1/2": [round(float(z), 4) for z in marginal_half],
|
| 249 |
-
"marginal at t = 1": [round(float(z), 4) for z in marginal_one],
|
| 250 |
-
"reverse rate, 2 back to 1": float(reverse[1, 0]),
|
| 251 |
-
"Euler error at the coarsest step": sampler[coarse][1],
|
| 252 |
-
"Euler error, step halved": sampler[2 * coarse][1],
|
| 253 |
-
"Euler error, step quartered": sampler[4 * coarse][1],
|
| 254 |
-
"Euler error at ten times the steps": sampler[10 * coarse][1],
|
| 255 |
-
"KL(pi* || R_01)": endpoint_cost,
|
| 256 |
-
"rate error integrand at t = 1/2": at_half,
|
| 257 |
-
"its integral over [0, 1]": integral,
|
| 258 |
-
"cost at lambda = 1/2": speeds[0.5],
|
| 259 |
-
"cost at lambda = 4": speeds[4.0],
|
| 260 |
-
"KL(pi_1 || the stationary law)": limit,
|
| 261 |
-
}
|
| 262 |
-
print_report("The bridge over a finite set, Chapter 7 Sections 7.21 to 7.23.3", report)
|
| 263 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
if __name__ == "__main__":
|
| 267 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/displacement.py
DELETED
|
@@ -1,237 +0,0 @@
|
|
| 1 |
-
"""Transport as motion in time: the kinetic action, straight lines, and displacement interpolation.
|
| 2 |
-
|
| 3 |
-
A plan records which source point ends where and nothing about the trajectory between them. The
|
| 4 |
-
Benamou-Brenier formulation replaces the plan by a density and a velocity field and measures the
|
| 5 |
-
transport by the energy the particles expend, and its minimizer moves every particle in a straight
|
| 6 |
-
line at constant speed with the endpoints paired by the optimal plan.
|
| 7 |
-
|
| 8 |
-
Chapter 7, Sections 7.7 to 7.7.5 of the notes. Runs on the one-particle example of Section 7.7.1
|
| 9 |
-
and on the Gaussian pair N(0,1) to N(3,4), both in closed form. Prints the action of a steady and a
|
| 10 |
-
dawdling particle, the two sides of the averaged bound, the interpolated density against the mixture
|
| 11 |
-
of the endpoints, and the velocity field with its action density.
|
| 12 |
-
|
| 13 |
-
python lecture_7/displacement.py # 1024 times on the grid
|
| 14 |
-
python lecture_7/displacement.py --steps 20 # a coarse time grid, for a first look
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
# %% 01. Imports and configuration
|
| 18 |
-
from __future__ import annotations
|
| 19 |
-
|
| 20 |
-
import math
|
| 21 |
-
|
| 22 |
-
import torch
|
| 23 |
-
|
| 24 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 25 |
-
|
| 26 |
-
DTYPE = torch.float64 # Closed forms throughout, so the printed digits are the chapter's.
|
| 27 |
-
|
| 28 |
-
MEAN_0, STD_0 = 0.0, 1.0 # The source N(0, 1).
|
| 29 |
-
MEAN_1, STD_1 = 3.0, 2.0 # The target N(3, 4), whose standard deviation is 2.
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
# %% 02. One particle
|
| 33 |
-
#
|
| 34 |
-
# The squared displacement of a particle is at most the integral of its squared speed, by
|
| 35 |
-
# Cauchy-Schwarz against the constant function 1 on an interval of length 1, with equality when the
|
| 36 |
-
# velocity is constant in time. Section 7.7.1.
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def action_of_a_path(positions: torch.Tensor, times: torch.Tensor) -> float:
|
| 40 |
-
"""The integral of the squared speed along a trajectory sampled at the given times."""
|
| 41 |
-
velocity = (positions[1:] - positions[:-1]) / (times[1:] - times[:-1]) # [n-1] piecewise speed.
|
| 42 |
-
return float((velocity ** 2 * (times[1:] - times[:-1])).sum()) # Integral of ||x_dot||^2.
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def steady_particle(times: torch.Tensor) -> torch.Tensor:
|
| 46 |
-
"""A particle covering the distance 2 at the constant speed 2, so its action is 4."""
|
| 47 |
-
return 2.0 * times # x(t) = 2t.
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def dawdling_particle(times: torch.Tensor) -> torch.Tensor:
|
| 51 |
-
"""A particle that waits until t = 1/2 and then covers the same distance at speed 4."""
|
| 52 |
-
return torch.clamp(4.0 * (times - 0.5), min=0.0) # Zero on [0, 1/2], then 4(t - 1/2).
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
# %% 03. Averaging the bound over the particles
|
| 56 |
-
#
|
| 57 |
-
# The joint law of the two endpoints of a particle is a coupling of mu and nu, so the action is at
|
| 58 |
-
# least the average squared displacement under that coupling, which is at least W_2^2.
|
| 59 |
-
# Section 7.7.2.
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def average_squared_displacement(x: torch.Tensor, y: torch.Tensor, pi: torch.Tensor) -> float:
|
| 63 |
-
"""The average of ||x_1 - x_0||^2 over endpoint pairs drawn from the coupling pi."""
|
| 64 |
-
return float((pi * (x[:, None] - y[None, :]) ** 2).sum()) # Sum over routes of mass times cost.
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
# %% 04. Straight lines attain the bound
|
| 68 |
-
#
|
| 69 |
-
# Drawing the endpoint pair from the optimal coupling and moving along the segment between them
|
| 70 |
-
# makes the velocity constant, so Cauchy-Schwarz is tight for every particle, and the coupling is
|
| 71 |
-
# optimal, so the second inequality is tight as well. Section 7.7.3.
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def straight_path(x0: torch.Tensor, x1: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 75 |
-
"""x(t) = (1 - t) x_0 + t x_1, whose velocity is the constant x_1 - x_0."""
|
| 76 |
-
return (1.0 - t) * x0 + t * x1 # Same shape as x0 and x1 broadcast against t.
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
# %% 05. Displacement interpolation
|
| 80 |
-
#
|
| 81 |
-
# Reading off the law of the particles at an intermediate time gives the displacement interpolation.
|
| 82 |
-
# Under the optimal map X_1 = 3 + 2 X_0 of the Gaussian example, X_t collects into 3t + (1 + t) X_0,
|
| 83 |
-
# so rho_t is Gaussian with mean 3t and standard deviation 1 + t. Section 7.7.4.
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def displacement_law(t: float) -> tuple[float, float]:
|
| 87 |
-
"""The mean and standard deviation of rho_t for the Gaussian pair, both linear in t."""
|
| 88 |
-
return 3.0 * t, 1.0 + t # Mean slides from 0 to 3, standard deviation from 1 to 2.
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def normal_density(x: torch.Tensor, mean: float, std: float) -> torch.Tensor:
|
| 92 |
-
"""The density of N(mean, std^2) evaluated at every entry of x."""
|
| 93 |
-
return torch.exp(-0.5 * ((x - mean) / std) ** 2) / (std * math.sqrt(2.0 * math.pi))
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def mixture_density(x: torch.Tensor) -> torch.Tensor:
|
| 97 |
-
"""Half the source density plus half the target density, the other path between the two ends."""
|
| 98 |
-
return 0.5 * normal_density(x, MEAN_0, STD_0) + 0.5 * normal_density(x, MEAN_1, STD_1)
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def gaussian_w2(m0: float, s0: float, m1: float, s1: float) -> float:
|
| 102 |
-
"""W_2 between two Gaussians on the line, the closed form of Section 7.6."""
|
| 103 |
-
return math.sqrt((m0 - m1) ** 2 + (s0 - s1) ** 2)
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
# %% 06. The velocity field behind the displacement path
|
| 107 |
-
#
|
| 108 |
-
# Each particle obeys X_t = 3t + (1 + t) X_0 with the constant velocity 3 + X_0. Solving the
|
| 109 |
-
# trajectory for X_0 and substituting makes the velocity a function of the current position alone.
|
| 110 |
-
# Section 7.7.5.
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def velocity_field(x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 114 |
-
"""v_t(x) = 3 + (x - 3t) / (1 + t), the mean speed plus a stretch about the mean."""
|
| 115 |
-
return 3.0 + (x - 3.0 * t) / (1.0 + t) # Broadcasts over a grid of states and times.
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
def continuity_residual(x: float, t: float) -> float:
|
| 119 |
-
"""The left side of d_t rho_t + d_x (rho_t v_t) = 0, computed by automatic differentiation."""
|
| 120 |
-
xs = torch.tensor(x, dtype=DTYPE, requires_grad=True)
|
| 121 |
-
ts = torch.tensor(t, dtype=DTYPE, requires_grad=True)
|
| 122 |
-
rho = normal_density(xs, 3.0 * ts, 1.0 + ts) # rho_t written with the two linear schedules.
|
| 123 |
-
flux = rho * velocity_field(xs, ts) # The probability flux rho_t v_t.
|
| 124 |
-
d_rho_dt = torch.autograd.grad(rho, ts, retain_graph=True)[0] # Keep the graph for the flux.
|
| 125 |
-
d_flux_dx = torch.autograd.grad(flux, xs)[0]
|
| 126 |
-
return float((d_rho_dt + d_flux_dx).detach())
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
def action_density(t: float, levels: int) -> float:
|
| 130 |
-
"""The integral of rho_t(x) v_t(x)^2 over x, which is E[(3 + X_0)^2] at every time."""
|
| 131 |
-
mean, std = displacement_law(t)
|
| 132 |
-
q = (torch.arange(levels, dtype=DTYPE) + 0.5) / levels # Quantile levels of rho_t.
|
| 133 |
-
x = mean + std * torch.special.ndtri(q) # [levels] a stratified sample of rho_t.
|
| 134 |
-
speed = velocity_field(x, torch.tensor(t, dtype=DTYPE)) # [levels].
|
| 135 |
-
return float((speed ** 2).mean()) # The average of the squared speed under rho_t.
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
# %% 07. The report
|
| 139 |
-
def main() -> None:
|
| 140 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 141 |
-
parser.set_defaults(steps=1024) # --steps is the number of times on the grid.
|
| 142 |
-
args = parser.parse_args()
|
| 143 |
-
set_seed(args.seed)
|
| 144 |
-
loud = not args.quiet
|
| 145 |
-
n = max(args.steps, 2)
|
| 146 |
-
times = torch.linspace(0.0, 1.0, n + 1, dtype=DTYPE) # [n+1] the time grid.
|
| 147 |
-
|
| 148 |
-
steady = action_of_a_path(steady_particle(times), times)
|
| 149 |
-
dawdle = action_of_a_path(dawdling_particle(times), times)
|
| 150 |
-
if loud:
|
| 151 |
-
print(f"One particle from 0 to 2 on a grid of {n} steps")
|
| 152 |
-
print(f" constant speed 2: action {steady:.3f}, squared displacement {2.0 ** 2:.0f}")
|
| 153 |
-
print(f" waiting then speed 4: action {dawdle:.3f}, the same two endpoints")
|
| 154 |
-
|
| 155 |
-
x = torch.tensor([0.0, 2.0], dtype=DTYPE) # Four points, mass 1/2 each.
|
| 156 |
-
y = torch.tensor([1.0, 3.0], dtype=DTYPE)
|
| 157 |
-
ordered = torch.tensor([[0.5, 0.0], [0.0, 0.5]], dtype=DTYPE)
|
| 158 |
-
crossed = torch.tensor([[0.0, 0.5], [0.5, 0.0]], dtype=DTYPE)
|
| 159 |
-
independent = torch.full((2, 2), 0.25, dtype=DTYPE)
|
| 160 |
-
w22_points = average_squared_displacement(x, y, ordered) # The optimal coupling on the line.
|
| 161 |
-
if loud:
|
| 162 |
-
print(f"\nAveraging over particles, four points with mass 1/2")
|
| 163 |
-
for name, pi in (("ordered", ordered), ("independent", independent), ("crossed", crossed)):
|
| 164 |
-
print(f" {name:<12s} E||x_1 - x_0||^2 = "
|
| 165 |
-
f"{average_squared_displacement(x, y, pi):.1f} >= W_2^2 = {w22_points:.1f}")
|
| 166 |
-
|
| 167 |
-
# Straight lines under the optimal coupling, with the endpoints of the Gaussian example.
|
| 168 |
-
q = (torch.arange(n, dtype=DTYPE) + 0.5) / n
|
| 169 |
-
x0 = MEAN_0 + STD_0 * torch.special.ndtri(q) # [n] a stratified sample of N(0, 1).
|
| 170 |
-
x1 = MEAN_1 + (STD_1 / STD_0) * (x0 - MEAN_0) # The Brenier map 3 + 2x, the optimal pairing.
|
| 171 |
-
paths = straight_path(x0[None, :], x1[None, :], times[:, None]) # [n+1, n] every trajectory.
|
| 172 |
-
speeds = (paths[1:] - paths[:-1]) / (times[1:] - times[:-1])[:, None] # [n, n] piecewise speed.
|
| 173 |
-
action = float((speeds ** 2 * (times[1:] - times[:-1])[:, None]).sum(dim=0).mean())
|
| 174 |
-
squared_displacement = float(((x1 - x0) ** 2).mean())
|
| 175 |
-
if loud:
|
| 176 |
-
print("\nStraight lines under the optimal coupling")
|
| 177 |
-
print(f" action averaged over the particles {action:.4f} "
|
| 178 |
-
f"average squared displacement {squared_displacement:.4f}")
|
| 179 |
-
|
| 180 |
-
mean_half, std_half = displacement_law(0.5)
|
| 181 |
-
grid = torch.linspace(-6.0, 9.0, 15001, dtype=DTYPE) # A dense grid for the two densities.
|
| 182 |
-
mixture = mixture_density(grid)
|
| 183 |
-
peak = float(grid[int(mixture.argmax())])
|
| 184 |
-
interpolated = normal_density(grid, mean_half, std_half)
|
| 185 |
-
separation = float(0.5 * (interpolated - mixture).abs().sum() * (grid[1] - grid[0]))
|
| 186 |
-
half_distance = gaussian_w2(MEAN_0, STD_0, mean_half, std_half)
|
| 187 |
-
whole_distance = gaussian_w2(MEAN_0, STD_0, MEAN_1, STD_1)
|
| 188 |
-
if loud:
|
| 189 |
-
print(f"\nAt t = 1/2 the displacement interpolation is N({mean_half:.1f}, "
|
| 190 |
-
f"{std_half ** 2:.2f}) with standard deviation {std_half:.1f}")
|
| 191 |
-
print(f" the mixture of the two ends peaks at x = {peak:.2f} and is not a Gaussian")
|
| 192 |
-
print(f" total variation between the two candidate densities {separation:.3f}")
|
| 193 |
-
print(f" W_2(rho_0, rho_1/2) = {half_distance:.3f} = "
|
| 194 |
-
f"{half_distance / whole_distance:.1f} x W_2(mu, nu) = {whole_distance:.3f}")
|
| 195 |
-
|
| 196 |
-
mean_speed = float(velocity_field(torch.tensor(3.0 * 0.5, dtype=DTYPE),
|
| 197 |
-
torch.tensor(0.5, dtype=DTYPE))) # A particle at the mean.
|
| 198 |
-
above_speed = float(velocity_field(torch.tensor(3.0 * 0.5 + 1.5, dtype=DTYPE),
|
| 199 |
-
torch.tensor(0.5, dtype=DTYPE))) # One standard deviation up.
|
| 200 |
-
residual = max(abs(continuity_residual(xx, tt))
|
| 201 |
-
for xx in (-1.0, 0.0, 1.5, 4.0) for tt in (0.1, 0.5, 0.9))
|
| 202 |
-
densities = [action_density(float(t), 2048) for t in torch.linspace(0.0, 1.0, 5, dtype=DTYPE)]
|
| 203 |
-
landing = x0 + 1.0 * velocity_field(x0, torch.zeros((), dtype=DTYPE)) # One Euler step of size 1.
|
| 204 |
-
one_step = float((landing - x1).abs().max()) # The step lands on the Brenier map 3 + 2 x_0.
|
| 205 |
-
if loud:
|
| 206 |
-
print(f"\nv_t(x) = 3 + (x - 3t) / (1 + t)")
|
| 207 |
-
print(f" at the mean, speed {mean_speed:.0f}; one standard deviation above, "
|
| 208 |
-
f"speed {above_speed:.0f}")
|
| 209 |
-
print(f" action density at five times {[round(d, 4) for d in densities]}")
|
| 210 |
-
print(f" largest continuity residual on the test states {residual:.2e}")
|
| 211 |
-
|
| 212 |
-
report = {
|
| 213 |
-
"action of the steady particle": steady,
|
| 214 |
-
"action of the dawdling particle": dawdle,
|
| 215 |
-
"squared displacement of both": 4.0,
|
| 216 |
-
"E||x1-x0||^2, ordered": average_squared_displacement(x, y, ordered),
|
| 217 |
-
"E||x1-x0||^2, independent": average_squared_displacement(x, y, independent),
|
| 218 |
-
"E||x1-x0||^2, crossed": average_squared_displacement(x, y, crossed),
|
| 219 |
-
"action of the straight paths": action,
|
| 220 |
-
"W_2^2 of the Gaussian pair": whole_distance ** 2,
|
| 221 |
-
"rho_1/2 mean, standard deviation": f"{mean_half:.1f}, {std_half:.1f}",
|
| 222 |
-
"peak of the mixture": peak,
|
| 223 |
-
"total variation of the two paths": separation,
|
| 224 |
-
"W_2(rho_0, rho_1/2)": half_distance,
|
| 225 |
-
"W_2(mu, nu)": whole_distance,
|
| 226 |
-
"speed at the mean": mean_speed,
|
| 227 |
-
"speed one deviation above": above_speed,
|
| 228 |
-
"action density, every time": max(densities),
|
| 229 |
-
"continuity equation residual": residual,
|
| 230 |
-
"one Euler step of size 1 minus T": one_step,
|
| 231 |
-
}
|
| 232 |
-
print_report("Transport as motion, Chapter 7 Sections 7.7 to 7.7.5", report)
|
| 233 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
if __name__ == "__main__":
|
| 237 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/dsb.py
DELETED
|
@@ -1,204 +0,0 @@
|
|
| 1 |
-
"""Diffusion Schrodinger Bridge: iterative proportional fitting run on path laws.
|
| 2 |
-
|
| 3 |
-
Each half-step of proportional fitting is a relative-entropy projection onto the set of path laws
|
| 4 |
-
with one marginal correct, and the chain rule solves it in closed form by keeping the conditional
|
| 5 |
-
law of the previous iterate and resetting that one marginal. The conditional law is the current
|
| 6 |
-
process run backward in time, so a half-step amounts to reversing the current process and
|
| 7 |
-
restarting it from the requested law at the other end.
|
| 8 |
-
|
| 9 |
-
Chapter 7, Section 7.19.1 of the notes. Runs the half-steps exactly on the two-state chain with
|
| 10 |
-
transition matrix A, source law (0.5, 0.5) and requested terminal law (0.2, 0.8), and checks the
|
| 11 |
-
diffusion time reversal in closed form on a Gaussian. Prints the backward matrices, the law after
|
| 12 |
-
each half-step, the forward matrices of the first round and the error at the free end.
|
| 13 |
-
|
| 14 |
-
python lecture_7/dsb.py # 4 rounds, the first one worked by hand
|
| 15 |
-
python lecture_7/dsb.py --steps 1 # the single round the chapter writes out
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
from __future__ import annotations
|
| 20 |
-
|
| 21 |
-
import math
|
| 22 |
-
|
| 23 |
-
import torch
|
| 24 |
-
|
| 25 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 26 |
-
|
| 27 |
-
DTYPE = torch.float64 # The chapter quotes six figures of the backward and forward matrices.
|
| 28 |
-
|
| 29 |
-
A = torch.tensor([[0.7, 0.3], [0.4, 0.6]], dtype=DTYPE) # The reference transition matrix.
|
| 30 |
-
PI_0 = torch.tensor([0.5, 0.5], dtype=DTYPE) # The source law.
|
| 31 |
-
PI_1 = torch.tensor([0.2, 0.8], dtype=DTYPE) # The requested terminal law.
|
| 32 |
-
TILTED_0 = torch.tensor([[0.599160, 0.400840], [0.299266, 0.700734]], dtype=DTYPE) # A^h_0.
|
| 33 |
-
TILTED_1 = torch.tensor([[0.307297, 0.692703], [0.112490, 0.887510]], dtype=DTYPE) # A^h_1.
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
# %% 02. A path law as a joint table
|
| 37 |
-
#
|
| 38 |
-
# With two states and two steps a path law is a table over the triple (x_0, x_1, x_2), so every
|
| 39 |
-
# projection below is an exact operation on eight numbers. Section 7.12.1.
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def reference_path_law(start: torch.Tensor, matrix: torch.Tensor, steps: int) -> torch.Tensor:
|
| 43 |
-
"""R(x_{0:N}) as a tensor with one axis per time, from the starting law and one matrix per step."""
|
| 44 |
-
law = start.clone() # [K] the law at time 0.
|
| 45 |
-
for _ in range(steps):
|
| 46 |
-
law = law.unsqueeze(-1) * matrix # Multiply by the transition matrix on a new axis.
|
| 47 |
-
return law # [K] * (N + 1).
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
def marginal(law: torch.Tensor, axis: int) -> torch.Tensor:
|
| 51 |
-
"""The law of the state at one time, summed over every other time."""
|
| 52 |
-
other = [d for d in range(law.dim()) if d != axis]
|
| 53 |
-
return law.sum(dim=other) # [K].
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def one_step_conditional(law: torch.Tensor, step: int) -> torch.Tensor:
|
| 57 |
-
"""P(x_{k+1} | x_k), the forward transition matrix the path law carries at that step."""
|
| 58 |
-
pair = law.sum(dim=[d for d in range(law.dim()) if d not in (step, step + 1)]) # [K, K].
|
| 59 |
-
return pair / pair.sum(dim=1, keepdim=True) # Normalize each row.
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def reverse_step(law: torch.Tensor, step: int) -> torch.Tensor:
|
| 63 |
-
"""P(x_k | x_{k+1}), Bayes' rule on one step, which is the reversal a network has to learn."""
|
| 64 |
-
pair = law.sum(dim=[d for d in range(law.dim()) if d not in (step, step + 1)]) # [K, K].
|
| 65 |
-
return (pair / pair.sum(dim=0, keepdim=True)).T # [K, K] indexed by the later state first.
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
# %% 03. The two half-steps
|
| 69 |
-
#
|
| 70 |
-
# The odd iterate keeps the conditional law of the previous iterate given the final state and
|
| 71 |
-
# replaces the marginal at time 1, and the even iterate does the same at time 0. Section 7.19.1.
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def fix_terminal(law: torch.Tensor, end: torch.Tensor) -> torch.Tensor:
|
| 75 |
-
"""P'(x_{0:N}) = pi_1(x_N) P(x_{0:N-1} | x_N), the projection onto the laws ending at pi_1."""
|
| 76 |
-
last = marginal(law, law.dim() - 1) # [K] the terminal marginal of the current law.
|
| 77 |
-
return law * (end / last) # Reweight every path by the ratio at its final state.
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def fix_initial(law: torch.Tensor, start: torch.Tensor) -> torch.Tensor:
|
| 81 |
-
"""P''(x_{0:N}) = pi_0(x_0) P'(x_{1:N} | x_0), the projection onto the laws starting at pi_0."""
|
| 82 |
-
first = marginal(law, 0) # [K] the initial marginal of the current law.
|
| 83 |
-
shape = [-1] + [1] * (law.dim() - 1) # Broadcast the ratio along the first axis.
|
| 84 |
-
return law * (start / first).reshape(shape)
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
def proportional_fitting(law: torch.Tensor, start: torch.Tensor, end: torch.Tensor,
|
| 88 |
-
rounds: int) -> tuple[torch.Tensor, list[float], list[torch.Tensor]]:
|
| 89 |
-
"""Alternate the two half-steps, returning the law, the terminal error and the law each round."""
|
| 90 |
-
errors: list[float] = []
|
| 91 |
-
iterates: list[torch.Tensor] = []
|
| 92 |
-
for _ in range(rounds):
|
| 93 |
-
law = fix_terminal(law, end) # One half-step: the terminal marginal is now exact.
|
| 94 |
-
law = fix_initial(law, start) # The other: the initial marginal is now exact.
|
| 95 |
-
errors.append(float((marginal(law, law.dim() - 1) - end).abs().sum()))
|
| 96 |
-
iterates.append(law.clone())
|
| 97 |
-
return law, errors, iterates
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
# %% 04. The reversal that a network learns
|
| 101 |
-
#
|
| 102 |
-
# On a finite state space the reversal is Bayes' rule on a matrix. On R^d it is the time reversal of
|
| 103 |
-
# a diffusion, whose drift is minus the forward drift plus epsilon times the score of the current
|
| 104 |
-
# marginal, and the score of the current iterate has no closed form. Section 7.19.1.
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
def reverse_drift(x: torch.Tensor, t: torch.Tensor, epsilon: float, variance_0: float
|
| 108 |
-
) -> torch.Tensor:
|
| 109 |
-
"""beta(x, t) = -b + epsilon grad log p_t for a driftless diffusion started from N(0, v_0)."""
|
| 110 |
-
variance = variance_0 + epsilon * t # The marginal of sqrt(epsilon) dB started from N(0, v_0).
|
| 111 |
-
return epsilon * (-x / variance) # The score of a centered Gaussian is -x over its variance.
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def reversal_check(epsilon: float, variance_0: float, steps: int, paths: int,
|
| 115 |
-
generator: torch.Generator) -> tuple[float, float]:
|
| 116 |
-
"""Run the reversal backward from p_1 and compare its variance at time 0 with v_0."""
|
| 117 |
-
dt = 1.0 / steps
|
| 118 |
-
variance_1 = variance_0 + epsilon * 1.0 # The forward marginal at time 1.
|
| 119 |
-
x = torch.randn(paths, dtype=DTYPE, generator=generator) * math.sqrt(variance_1)
|
| 120 |
-
for step in range(steps):
|
| 121 |
-
t = torch.tensor(1.0 - step * dt, dtype=DTYPE) # Time runs down from 1 to 0.
|
| 122 |
-
noise = torch.randn(paths, dtype=DTYPE, generator=generator) * math.sqrt(dt)
|
| 123 |
-
x = x + reverse_drift(x, t, epsilon, variance_0) * dt + math.sqrt(epsilon) * noise
|
| 124 |
-
return float(x.var()), variance_0
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
# %% 05. The report
|
| 128 |
-
def main() -> None:
|
| 129 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 130 |
-
parser.set_defaults(steps=4) # --steps is the number of proportional fitting rounds.
|
| 131 |
-
args = parser.parse_args()
|
| 132 |
-
set_seed(args.seed)
|
| 133 |
-
loud = not args.quiet
|
| 134 |
-
rounds = max(args.steps, 1)
|
| 135 |
-
generator = torch.Generator().manual_seed(args.seed)
|
| 136 |
-
|
| 137 |
-
reference = reference_path_law(PI_0, A, 2) # [2, 2, 2] the law P^0 = R.
|
| 138 |
-
marginals = [marginal(reference, k) for k in range(3)]
|
| 139 |
-
B2 = reverse_step(reference, 1) # P(x_1 | x_2), the reversal of the last step.
|
| 140 |
-
B1 = reverse_step(reference, 0) # P(x_0 | x_1), the reversal of the earlier step.
|
| 141 |
-
if loud:
|
| 142 |
-
print("The reference started from pi_0, read at the three time points")
|
| 143 |
-
print(f" {[[round(float(z), 3) for z in m] for m in marginals]}")
|
| 144 |
-
print(f" B_2 = {[[round(float(z), 6) for z in row] for row in B2]}")
|
| 145 |
-
print(f" B_1(1,1) = {float(B1[0, 0]):.6f}")
|
| 146 |
-
|
| 147 |
-
half = fix_terminal(reference, PI_1) # The first half-step, which resets the terminal marginal.
|
| 148 |
-
backward_laws = [marginal(half, 2), marginal(half, 1), marginal(half, 0)]
|
| 149 |
-
drift_at_zero = float((backward_laws[2] - PI_0).abs().sum())
|
| 150 |
-
full = fix_initial(half, PI_0) # The second half-step, which resets the initial marginal.
|
| 151 |
-
F0 = one_step_conditional(full, 0)
|
| 152 |
-
F1 = one_step_conditional(full, 1)
|
| 153 |
-
terminal = marginal(full, 2)
|
| 154 |
-
terminal_error = float((terminal - PI_1).abs().sum())
|
| 155 |
-
tilt_gap = max(float((F0 - TILTED_0).abs().max()), float((F1 - TILTED_1).abs().max()))
|
| 156 |
-
if loud:
|
| 157 |
-
print("\nThe backward chain started from the requested terminal law")
|
| 158 |
-
written = ["(" + ", ".join(f"{float(z):.6f}" for z in m) + ")" for m in backward_laws]
|
| 159 |
-
print(f" {' -> '.join(written)}")
|
| 160 |
-
print(f" total absolute error at step 0: {drift_at_zero:.4f}")
|
| 161 |
-
print(f"\nThe forward matrices of the second half-step")
|
| 162 |
-
print(f" F_0 = {[[round(float(z), 6) for z in row] for row in F0]}")
|
| 163 |
-
print(f" F_1 = {[[round(float(z), 6) for z in row] for row in F1]}")
|
| 164 |
-
print(f" terminal law {[round(float(z), 6) for z in terminal]}, "
|
| 165 |
-
f"off by {terminal_error:.4f}")
|
| 166 |
-
print(f" largest gap against the tilted matrices A^h_0 and A^h_1: {tilt_gap:.4f}")
|
| 167 |
-
|
| 168 |
-
_, errors, _ = proportional_fitting(reference, PI_0, PI_1, rounds)
|
| 169 |
-
ratios = [errors[k] / errors[k + 1] for k in range(len(errors) - 1) if errors[k + 1] > 0]
|
| 170 |
-
if loud:
|
| 171 |
-
print(f"\nterminal error by round {['%.3e' % e for e in errors]}")
|
| 172 |
-
print(f" ratios {[round(r, 1) for r in ratios]}")
|
| 173 |
-
|
| 174 |
-
simulated, exact = reversal_check(1.0, 1.0, 200, 8192, generator)
|
| 175 |
-
if loud:
|
| 176 |
-
print(f"\nThe diffusion reversal on a Gaussian, run backward from p_1")
|
| 177 |
-
print(f" variance at time 0: simulated {simulated:.3f}, exact {exact:.3f}")
|
| 178 |
-
|
| 179 |
-
report = {
|
| 180 |
-
"reference marginal at step 1": [round(float(z), 3) for z in marginals[1]],
|
| 181 |
-
"reference marginal at step 2": [round(float(z), 3) for z in marginals[2]],
|
| 182 |
-
"B_2(1,1)": float(B2[0, 0]),
|
| 183 |
-
"B_2(2,1)": float(B2[1, 0]),
|
| 184 |
-
"B_1(1,1)": float(B1[0, 0]),
|
| 185 |
-
"backward law after one step": [round(float(z), 6) for z in backward_laws[1]],
|
| 186 |
-
"backward law at step 0": [round(float(z), 6) for z in backward_laws[2]],
|
| 187 |
-
"error at the free end, half-step": drift_at_zero,
|
| 188 |
-
"F_0(1,1)": float(F0[0, 0]),
|
| 189 |
-
"F_1(1,2)": float(F1[0, 1]),
|
| 190 |
-
"terminal law after one round": [round(float(z), 6) for z in terminal],
|
| 191 |
-
"terminal error after one round": terminal_error,
|
| 192 |
-
"gap against the tilted matrices": tilt_gap,
|
| 193 |
-
"terminal error, last round": errors[-1],
|
| 194 |
-
"error ratio per round": ratios[0] if ratios else float("nan"),
|
| 195 |
-
"rounds run": rounds,
|
| 196 |
-
"reversal variance, simulated": simulated,
|
| 197 |
-
"reversal variance, exact": exact,
|
| 198 |
-
}
|
| 199 |
-
print_report("Diffusion Schrodinger Bridge, Chapter 7 Section 7.19.1", report)
|
| 200 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
if __name__ == "__main__":
|
| 204 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/dsbm.py
DELETED
|
@@ -1,207 +0,0 @@
|
|
| 1 |
-
"""Diffusion Schrodinger Bridge Matching: the Markov projection and the reciprocal projection.
|
| 2 |
-
|
| 3 |
-
A mixture of bridges draws a pair of ends from a coupling and runs the reference bridge between
|
| 4 |
-
them, so its drift depends on the endpoint and the law is in general not Markov. Averaging that
|
| 5 |
-
drift over the endpoints of all bridges through a state removes the extra information and leaves a
|
| 6 |
-
diffusion with the same marginals, and replacing the bridges of a Markov law by reference bridges
|
| 7 |
-
keeps its endpoint coupling. Alternating the two projections holds both marginals fixed throughout
|
| 8 |
-
and improves only the pairing.
|
| 9 |
-
|
| 10 |
-
Chapter 7, Section 7.19.2 of the notes. Runs the two projections exactly on the two-state chain,
|
| 11 |
-
started from the independent coupling of (0.5, 0.5) and (0.2, 0.8). Prints the averaged drift at one
|
| 12 |
-
point, the two projected matrices of the first round, and the gap against the bridge coupling.
|
| 13 |
-
|
| 14 |
-
python lecture_7/dsbm.py # 9 rounds, which reach 1e-9 on this problem
|
| 15 |
-
python lecture_7/dsbm.py --steps 1 # the single round the chapter writes out
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
from __future__ import annotations
|
| 20 |
-
|
| 21 |
-
import torch
|
| 22 |
-
|
| 23 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 24 |
-
|
| 25 |
-
DTYPE = torch.float64 # The chapter quotes six figures of the two projected matrices.
|
| 26 |
-
|
| 27 |
-
A = torch.tensor([[0.7, 0.3], [0.4, 0.6]], dtype=DTYPE) # The reference transition matrix.
|
| 28 |
-
PI_0 = torch.tensor([0.5, 0.5], dtype=DTYPE) # The source law.
|
| 29 |
-
PI_1 = torch.tensor([0.2, 0.8], dtype=DTYPE) # The requested terminal law.
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
# %% 02. The averaged bridge drift
|
| 33 |
-
#
|
| 34 |
-
# Two bridges passing through the same state at the same time carry different drifts, and the
|
| 35 |
-
# Markov drift at that state is their average weighted by how often each one passes through.
|
| 36 |
-
# A least-squares fit of a single number against the two targets lands on the same value.
|
| 37 |
-
# Section 7.19.2.
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def bridge_drift(x1: float, x: float, t: float) -> float:
|
| 41 |
-
"""(x_1 - x) / (1 - t), the drift of the Brownian bridge ending at x_1."""
|
| 42 |
-
return (x1 - x) / (1.0 - t)
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def markov_drift(endpoints: torch.Tensor, weights: torch.Tensor, x: float, t: float) -> float:
|
| 46 |
-
"""v*(x, t) = E[(X_1 - x) / (1 - t) | X_t = x], the average over the bridges through x."""
|
| 47 |
-
drifts = (endpoints - x) / (1.0 - t) # [n] one conditional drift per endpoint.
|
| 48 |
-
return float((weights * drifts).sum()) # The posterior average of the conditional drifts.
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def least_squares_derivative(a: float, endpoints: torch.Tensor, weights: torch.Tensor,
|
| 52 |
-
x: float, t: float) -> float:
|
| 53 |
-
"""The derivative in a of the average squared error of a single prediction against the drifts."""
|
| 54 |
-
drifts = (endpoints - x) / (1.0 - t) # [n].
|
| 55 |
-
return float(2.0 * (weights * (a - drifts)).sum()) # Vanishes at the weighted average.
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
# %% 03. The reciprocal projection
|
| 59 |
-
#
|
| 60 |
-
# A member of the reciprocal class is an endpoint coupling filled in with reference bridges. The
|
| 61 |
-
# projection of a Markov law keeps that law's endpoint coupling and redraws the interior from the
|
| 62 |
-
# reference. Section 7.19.2.
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def reference_bridge_table(matrix: torch.Tensor) -> torch.Tensor:
|
| 66 |
-
"""R(x_1 | x_0, x_2) = A(x_0, x_1) A(x_1, x_2) / A^2(x_0, x_2) for every triple. [K, K, K]."""
|
| 67 |
-
two_step = matrix @ matrix # [K, K] the reference over both steps.
|
| 68 |
-
joint = matrix[:, :, None] * matrix[None, :, :] # [x_0, x_1, x_2] the unnormalized weight.
|
| 69 |
-
return joint / two_step[:, None, :] # Normalize over the middle state.
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def reciprocal_projection(coupling: torch.Tensor, matrix: torch.Tensor) -> torch.Tensor:
|
| 73 |
-
"""proj_R(M) = M_{0,1} R_{.|0,1}, the coupling with the reference bridges put back in."""
|
| 74 |
-
return coupling[:, None, :] * reference_bridge_table(matrix) # [K, K, K] a full path law.
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
# %% 04. The Markov projection
|
| 78 |
-
#
|
| 79 |
-
# Retaining the one-step conditionals of a path law and multiplying them into a chain gives the
|
| 80 |
-
# Markov law with the same marginals, and the averaging in each conditional is the discrete form of
|
| 81 |
-
# the drift average. Section 7.19.2.
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def one_step_conditional(law: torch.Tensor, step: int) -> torch.Tensor:
|
| 85 |
-
"""P(x_{k+1} | x_k) read off a path law, which is what the network fits at that transition."""
|
| 86 |
-
pair = law.sum(dim=[d for d in range(law.dim()) if d not in (step, step + 1)]) # [K, K].
|
| 87 |
-
return pair / pair.sum(dim=1, keepdim=True)
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def markov_projection(law: torch.Tensor) -> tuple[list[torch.Tensor], torch.Tensor]:
|
| 91 |
-
"""The chain of one-step conditionals, and the endpoint coupling it produces from pi_0."""
|
| 92 |
-
conditionals = [one_step_conditional(law, k) for k in range(law.dim() - 1)]
|
| 93 |
-
joint = torch.diag(marginal(law, 0)) # [K, K] the initial law on the diagonal.
|
| 94 |
-
for matrix in conditionals:
|
| 95 |
-
joint = joint @ matrix # Multiply the conditionals into a chain.
|
| 96 |
-
return conditionals, joint
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
def marginal(law: torch.Tensor, axis: int) -> torch.Tensor:
|
| 100 |
-
"""The law of the state at one time, summed over every other time."""
|
| 101 |
-
return law.sum(dim=[d for d in range(law.dim()) if d != axis])
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
# %% 05. Alternating the two projections
|
| 105 |
-
#
|
| 106 |
-
# Under a Markov projection every marginal is unchanged and under a reciprocal projection the
|
| 107 |
-
# endpoint coupling is unchanged, so both pi_0 and pi_1 stay exact once the starting law has them,
|
| 108 |
-
# and the iterations improve only the coupling. Section 7.19.2.
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def imf_rounds(coupling: torch.Tensor, matrix: torch.Tensor, rounds: int,
|
| 112 |
-
target: torch.Tensor) -> tuple[torch.Tensor, list[float], list]:
|
| 113 |
-
"""Run iterative Markovian fitting, returning the coupling, the gaps and the first round."""
|
| 114 |
-
gaps: list[float] = []
|
| 115 |
-
first: list = []
|
| 116 |
-
for index in range(rounds):
|
| 117 |
-
law = reciprocal_projection(coupling, matrix) # Reference bridges between the current ends.
|
| 118 |
-
conditionals, coupling = markov_projection(law) # The Markov law and its endpoint coupling.
|
| 119 |
-
gaps.append(float((coupling - target).abs().sum()))
|
| 120 |
-
if index == 0:
|
| 121 |
-
first = conditionals
|
| 122 |
-
return coupling, gaps, first
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def bridge_coupling(matrix: torch.Tensor, start: torch.Tensor, end: torch.Tensor,
|
| 126 |
-
rounds: int = 200) -> torch.Tensor:
|
| 127 |
-
"""The exact bridge coupling, by scaling diag(pi_0) A^2 to the two marginals."""
|
| 128 |
-
R01 = torch.diag(start) @ torch.linalg.matrix_power(matrix, 2)
|
| 129 |
-
f = torch.ones_like(start)
|
| 130 |
-
for _ in range(rounds):
|
| 131 |
-
g = end / (R01.T @ f)
|
| 132 |
-
f = start / (R01 @ g)
|
| 133 |
-
return f[:, None] * R01 * g[None, :]
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
# %% 06. The report
|
| 137 |
-
def main() -> None:
|
| 138 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 139 |
-
parser.set_defaults(steps=9) # --steps is the number of Markovian fitting rounds.
|
| 140 |
-
args = parser.parse_args()
|
| 141 |
-
set_seed(args.seed)
|
| 142 |
-
loud = not args.quiet
|
| 143 |
-
rounds = max(args.steps, 1)
|
| 144 |
-
|
| 145 |
-
endpoints = torch.tensor([3.0, 0.0], dtype=DTYPE) # Two bridges through x = 1 at t = 0.5.
|
| 146 |
-
weights = torch.tensor([0.5, 0.5], dtype=DTYPE) # They pass with equal probability.
|
| 147 |
-
average = markov_drift(endpoints, weights, 1.0, 0.5)
|
| 148 |
-
if loud:
|
| 149 |
-
print("Two bridges through x = 1 at t = 0.5, ending at 3 and at 0")
|
| 150 |
-
print(f" their drifts {bridge_drift(3.0, 1.0, 0.5):.0f} and "
|
| 151 |
-
f"{bridge_drift(0.0, 1.0, 0.5):.0f}, average {average:.0f}")
|
| 152 |
-
print(f" derivative of the squared error at a = {average:.0f}: "
|
| 153 |
-
f"{least_squares_derivative(average, endpoints, weights, 1.0, 0.5):.0f}")
|
| 154 |
-
print(f" derivative at a = 0: "
|
| 155 |
-
f"{least_squares_derivative(0.0, endpoints, weights, 1.0, 0.5):+.0f}")
|
| 156 |
-
|
| 157 |
-
independent = PI_0[:, None] * PI_1[None, :] # [[0.1, 0.4], [0.1, 0.4]].
|
| 158 |
-
target = bridge_coupling(A, PI_0, PI_1)
|
| 159 |
-
bridges = reference_bridge_table(A)
|
| 160 |
-
starting_gap = float((independent - target).abs().sum())
|
| 161 |
-
coupling, gaps, first = imf_rounds(independent, A, rounds, target)
|
| 162 |
-
projected = torch.diag(PI_0) @ first[0] @ first[1]
|
| 163 |
-
ratios = [gaps[k] / gaps[k + 1] for k in range(len(gaps) - 1) if gaps[k + 1] > 0]
|
| 164 |
-
if loud:
|
| 165 |
-
print(f"\nStarting from the independent coupling "
|
| 166 |
-
f"{[[round(float(z), 1) for z in row] for row in independent]}")
|
| 167 |
-
print(f" the middle state is 1 with probability {float(bridges[0, 0, 0]):.6f} "
|
| 168 |
-
f"from 1 to 1, and {float(bridges[0, 0, 1]):.6f} from 1 to 2")
|
| 169 |
-
print(f" M_0 = {[[round(float(z), 6) for z in row] for row in first[0]]}")
|
| 170 |
-
print(f" M_1 = {[[round(float(z), 6) for z in row] for row in first[1]]}")
|
| 171 |
-
print(f" M_0(1,1) = 0.2 x {float(bridges[0, 0, 0]):.6f} + 0.8 x "
|
| 172 |
-
f"{float(bridges[0, 0, 1]):.6f} = {float(first[0][0, 0]):.6f}")
|
| 173 |
-
print(f" diag(pi_0) M_0 M_1 = "
|
| 174 |
-
f"{[[round(float(z), 6) for z in row] for row in projected]}")
|
| 175 |
-
print(f" row sums {[round(float(z), 3) for z in projected.sum(1)]}, "
|
| 176 |
-
f"column sums {[round(float(z), 3) for z in projected.sum(0)]}")
|
| 177 |
-
print(f"\ngap against pi*: {starting_gap:.4f} at the start, "
|
| 178 |
-
f"then {['%.2e' % g for g in gaps[:5]]}")
|
| 179 |
-
print(f" ratios {[round(r, 2) for r in ratios[:4]]}")
|
| 180 |
-
|
| 181 |
-
reached = next((k + 1 for k, g in enumerate(gaps) if g < 1e-9), None)
|
| 182 |
-
report = {
|
| 183 |
-
"drift of the bridge ending at 3": bridge_drift(3.0, 1.0, 0.5),
|
| 184 |
-
"drift of the bridge ending at 0": bridge_drift(0.0, 1.0, 0.5),
|
| 185 |
-
"averaged Markov drift": average,
|
| 186 |
-
"least squares derivative at a = 1": least_squares_derivative(
|
| 187 |
-
average, endpoints, weights, 1.0, 0.5),
|
| 188 |
-
"R(mid = 1 | 1, 1)": float(bridges[0, 0, 0]),
|
| 189 |
-
"R(mid = 1 | 1, 2)": float(bridges[0, 0, 1]),
|
| 190 |
-
"M_0(1,1)": float(first[0][0, 0]),
|
| 191 |
-
"M_1(1,2)": float(first[1][0, 1]),
|
| 192 |
-
"endpoint coupling, entry 11": float(projected[0, 0]),
|
| 193 |
-
"endpoint coupling, entry 12": float(projected[0, 1]),
|
| 194 |
-
"row sums after the projection": [round(float(z), 6) for z in projected.sum(dim=1)],
|
| 195 |
-
"column sums after the projection": [round(float(z), 6) for z in projected.sum(dim=0)],
|
| 196 |
-
"gap, independent coupling": starting_gap,
|
| 197 |
-
"gap after one round": gaps[0],
|
| 198 |
-
"gap after the last round": gaps[-1],
|
| 199 |
-
"gap ratio per round": ratios[-1] if ratios else float("nan"),
|
| 200 |
-
"rounds to 1e-9": reached if reached else rounds,
|
| 201 |
-
}
|
| 202 |
-
print_report("Diffusion Schrodinger Bridge Matching, Chapter 7 Section 7.19.2", report)
|
| 203 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
if __name__ == "__main__":
|
| 207 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/dynamic_bridge.py
DELETED
|
@@ -1,257 +0,0 @@
|
|
| 1 |
-
"""A diffusion reference: Girsanov, the control problem, Hopf-Cole, and the Brownian bridge.
|
| 2 |
-
|
| 3 |
-
Changing the drift of a diffusion and keeping its noise is the only change under which the two path
|
| 4 |
-
laws stay comparable, and over one short step the two increments differ only in their mean. Summing
|
| 5 |
-
those steps gives Girsanov's formula, averaging it under the controlled law turns relative entropy
|
| 6 |
-
on paths into an integral of a squared drift, and the Hopf-Cole change of variables turns the
|
| 7 |
-
resulting Hamilton-Jacobi-Bellman equation into the linear backward equation of the reference.
|
| 8 |
-
|
| 9 |
-
Chapter 7, Sections 7.16 to 7.18 of the notes. Runs on a driftless diffusion in one dimension.
|
| 10 |
-
Prints the two computations of the relative entropy of a constant control, the residual of the
|
| 11 |
-
backward equation and of the Hamilton-Jacobi-Bellman equation, and the Brownian bridge between two
|
| 12 |
-
fixed points.
|
| 13 |
-
|
| 14 |
-
python lecture_7/dynamic_bridge.py # 200 Euler-Maruyama steps
|
| 15 |
-
python lecture_7/dynamic_bridge.py --steps 20 # a coarse time grid, for a first look
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
from __future__ import annotations
|
| 20 |
-
|
| 21 |
-
import math
|
| 22 |
-
|
| 23 |
-
import torch
|
| 24 |
-
|
| 25 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 26 |
-
|
| 27 |
-
DTYPE = torch.float64 # Closed forms and a Monte Carlo check, both in double precision.
|
| 28 |
-
|
| 29 |
-
EPSILON = 1.0 # The reference noise variance per unit time.
|
| 30 |
-
START, END = 0.0, 2.0 # The two points the bridge is pinned at.
|
| 31 |
-
PATHS = 8192 # Trajectories in the Monte Carlo checks.
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
# %% 02. One short step
|
| 35 |
-
#
|
| 36 |
-
# Over a step of length dt both increments are Gaussian with the covariance epsilon dt, and they
|
| 37 |
-
# differ only in the mean, by u dt. The logarithm of the ratio of the two densities is the inner
|
| 38 |
-
# product of w, the increment beyond the reference drift, with u over epsilon, minus the squared
|
| 39 |
-
# norm of u times dt over 2 epsilon. Section 7.16.1.
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def log_density_ratio_step(w: torch.Tensor, u: torch.Tensor, dt: float,
|
| 43 |
-
epsilon: float = EPSILON) -> torch.Tensor:
|
| 44 |
-
"""log p^u / p^R over one step, with w = Delta X - b dt the part beyond the reference drift."""
|
| 45 |
-
return (w * u).sum(dim=-1) / epsilon - (u ** 2).sum(dim=-1) * dt / (2.0 * epsilon)
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
# %% 03. Summing over the steps, and the average under the controlled law
|
| 49 |
-
#
|
| 50 |
-
# Adding the per-step contributions and letting dt go to zero gives Girsanov's formula, a martingale
|
| 51 |
-
# term built from the noise and a drift term built from the size of the control. Averaging under
|
| 52 |
-
# P^u leaves the two energy terms with coefficients 1/epsilon and -1/(2 epsilon).
|
| 53 |
-
# Sections 7.16.2 and 7.16.3.
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def simulate_controlled(u: float, steps: int, paths: int, epsilon: float = EPSILON,
|
| 57 |
-
generator: torch.Generator | None = None) -> tuple[torch.Tensor, torch.Tensor]:
|
| 58 |
-
"""Euler-Maruyama for dX = u dt + sqrt(epsilon) dB, returning the endpoints and the log ratio.
|
| 59 |
-
|
| 60 |
-
The log ratio is accumulated step by step from the same Brownian increments the path uses, which
|
| 61 |
-
is how Section 7.27.3 accumulates it during training.
|
| 62 |
-
"""
|
| 63 |
-
dt = 1.0 / steps
|
| 64 |
-
x = torch.zeros(paths, 1, dtype=DTYPE) # [P, 1] every path starts at the origin.
|
| 65 |
-
log_ratio = torch.zeros(paths, dtype=DTYPE) # [P] the Girsanov integral along each path.
|
| 66 |
-
control = torch.full((paths, 1), u, dtype=DTYPE) # [P, 1] a control constant in space and time.
|
| 67 |
-
for _ in range(steps):
|
| 68 |
-
noise = torch.randn(paths, 1, dtype=DTYPE, generator=generator) * math.sqrt(dt) # dB.
|
| 69 |
-
w = control * dt + math.sqrt(epsilon) * noise # Delta X with no reference drift.
|
| 70 |
-
log_ratio = log_ratio + log_density_ratio_step(w, control, dt, epsilon)
|
| 71 |
-
x = x + w # The controlled state after this step.
|
| 72 |
-
return x.squeeze(1), log_ratio
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def control_energy_entropy(u: float, epsilon: float = EPSILON) -> float:
|
| 76 |
-
"""KL(P^u || R) = (1 / 2 epsilon) E int_0^1 ||u||^2 dt, for a control constant in time."""
|
| 77 |
-
return u ** 2 / (2.0 * epsilon) # The integral of a constant over the unit interval.
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
def gaussian_entropy_gap(shift: float, variance: float) -> float:
|
| 81 |
-
"""KL between two Gaussians with a common variance, the squared difference of the means over 2v."""
|
| 82 |
-
return shift ** 2 / (2.0 * variance)
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
# %% 04. The value function and the optimal control
|
| 86 |
-
#
|
| 87 |
-
# Bellman's principle and Ito's formula give the Hamilton-Jacobi-Bellman equation, in which only two
|
| 88 |
-
# terms contain the control. Completing the square in those two isolates u* = -grad V, and
|
| 89 |
-
# substituting it back leaves a closed equation for V with a squared gradient in it.
|
| 90 |
-
# Sections 7.17 to 7.17.2.
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
def optimal_control(value, x: float, t: float) -> float:
|
| 94 |
-
"""u*(x, t) = -grad V_t(x), the control pointing downhill on the cost-to-go."""
|
| 95 |
-
xs = torch.tensor(x, dtype=DTYPE, requires_grad=True)
|
| 96 |
-
v = value(xs, torch.tensor(t, dtype=DTYPE))
|
| 97 |
-
return float(-torch.autograd.grad(v, xs)[0])
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def hjb_residual(value, x: float, t: float, epsilon: float = EPSILON) -> float:
|
| 101 |
-
"""d_t V + (epsilon / 2) grad^2 V - (1/2) ||grad V||^2, with no reference drift."""
|
| 102 |
-
xs = torch.tensor(x, dtype=DTYPE, requires_grad=True)
|
| 103 |
-
ts = torch.tensor(t, dtype=DTYPE, requires_grad=True)
|
| 104 |
-
v = value(xs, ts)
|
| 105 |
-
dv_dt = torch.autograd.grad(v, ts, create_graph=True)[0]
|
| 106 |
-
dv_dx = torch.autograd.grad(v, xs, create_graph=True)[0]
|
| 107 |
-
d2v_dx2 = torch.autograd.grad(dv_dx, xs, create_graph=True)[0]
|
| 108 |
-
return float((dv_dt + 0.5 * epsilon * d2v_dx2 - 0.5 * dv_dx ** 2).detach())
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
# %% 05. Removing the squared gradient
|
| 112 |
-
#
|
| 113 |
-
# Writing the value function as minus epsilon times a logarithm cancels the squared gradient against
|
| 114 |
-
# the extra term the Laplacian produces, and what is left is the Kolmogorov backward equation of the
|
| 115 |
-
# reference, which is linear. In the new variable the control is epsilon times the gradient of the
|
| 116 |
-
# logarithm of phi. Sections 7.17.3 and 7.17.4.
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def hopf_cole(potential, epsilon: float = EPSILON):
|
| 120 |
-
"""V_t = -epsilon log phi_t, the change of variables that linearizes the equation."""
|
| 121 |
-
return lambda x, t: -epsilon * torch.log(potential(x, t))
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
def backward_residual(potential, x: float, t: float, epsilon: float = EPSILON) -> float:
|
| 125 |
-
"""d_t phi + (epsilon / 2) grad^2 phi, which vanishes for a reference transition density."""
|
| 126 |
-
xs = torch.tensor(x, dtype=DTYPE, requires_grad=True)
|
| 127 |
-
ts = torch.tensor(t, dtype=DTYPE, requires_grad=True)
|
| 128 |
-
phi = potential(xs, ts)
|
| 129 |
-
dphi_dt = torch.autograd.grad(phi, ts, create_graph=True)[0]
|
| 130 |
-
dphi_dx = torch.autograd.grad(phi, xs, create_graph=True)[0]
|
| 131 |
-
d2phi_dx2 = torch.autograd.grad(dphi_dx, xs, create_graph=True)[0]
|
| 132 |
-
return float((dphi_dt + 0.5 * epsilon * d2phi_dx2).detach())
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
# %% 06. The Brownian bridge
|
| 136 |
-
#
|
| 137 |
-
# Conditioning on a single final point y makes the potential the reference density of arriving at y
|
| 138 |
-
# from the current state, and epsilon times the gradient of its logarithm is the remaining
|
| 139 |
-
# displacement over the remaining time. The noise level cancels from the drift and survives in the
|
| 140 |
-
# variance. Section 7.18.
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
def bridge_potential(z: torch.Tensor, t: torch.Tensor, y: float = END,
|
| 144 |
-
epsilon: float = EPSILON) -> torch.Tensor:
|
| 145 |
-
"""h(z, t) = R_{1|t}(y | z), the full Gaussian density with variance epsilon (1 - t)."""
|
| 146 |
-
remaining = 1.0 - t # The time left, which is the variance divided by epsilon.
|
| 147 |
-
return torch.exp(-((y - z) ** 2) / (2.0 * epsilon * remaining)) / torch.sqrt(
|
| 148 |
-
2.0 * math.pi * epsilon * remaining)
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
def bridge_drift(z: torch.Tensor, t: torch.Tensor, y: float = END) -> torch.Tensor:
|
| 152 |
-
"""(y - z) / (1 - t), the velocity that reaches y on time if nothing further perturbs the state."""
|
| 153 |
-
return (y - z) / (1.0 - t)
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
def bridge_law(t: float, x: float = START, y: float = END,
|
| 157 |
-
epsilon: float = EPSILON) -> tuple[float, float]:
|
| 158 |
-
"""The mean (1 - t) x + t y and the variance epsilon t (1 - t) of the bridge at time t."""
|
| 159 |
-
return (1.0 - t) * x + t * y, epsilon * t * (1.0 - t)
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def simulate_bridge(steps: int, paths: int, epsilon: float = EPSILON,
|
| 163 |
-
generator: torch.Generator | None = None) -> torch.Tensor:
|
| 164 |
-
"""Euler-Maruyama for the bridge SDE, returning the state at every time. [steps + 1, P]."""
|
| 165 |
-
dt = 1.0 / steps
|
| 166 |
-
x = torch.full((paths,), START, dtype=DTYPE)
|
| 167 |
-
trajectory = [x.clone()]
|
| 168 |
-
for step in range(steps):
|
| 169 |
-
t = torch.tensor(step * dt, dtype=DTYPE)
|
| 170 |
-
noise = torch.randn(paths, dtype=DTYPE, generator=generator) * math.sqrt(dt)
|
| 171 |
-
x = x + bridge_drift(x, t) * dt + math.sqrt(epsilon) * noise # One Euler-Maruyama step.
|
| 172 |
-
trajectory.append(x.clone())
|
| 173 |
-
return torch.stack(trajectory) # [steps + 1, P].
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
# %% 07. The report
|
| 177 |
-
def main() -> None:
|
| 178 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 179 |
-
parser.set_defaults(steps=200) # --steps is the number of Euler-Maruyama steps.
|
| 180 |
-
args = parser.parse_args()
|
| 181 |
-
set_seed(args.seed)
|
| 182 |
-
loud = not args.quiet
|
| 183 |
-
steps = max(args.steps, 2)
|
| 184 |
-
generator = torch.Generator().manual_seed(args.seed)
|
| 185 |
-
|
| 186 |
-
endpoints, log_ratio = simulate_controlled(2.0, steps, PATHS, EPSILON, generator)
|
| 187 |
-
girsanov = float(log_ratio.mean()) # The average of the log density under P^u.
|
| 188 |
-
closed_form = control_energy_entropy(2.0)
|
| 189 |
-
endpoint_form = gaussian_entropy_gap(2.0, EPSILON)
|
| 190 |
-
if loud:
|
| 191 |
-
print(f"A constant control u = 2 at epsilon = {EPSILON:g}, over {steps} steps")
|
| 192 |
-
print(f" (1 / 2 eps) int ||u||^2 dt = {closed_form:.3f}")
|
| 193 |
-
print(f" KL(N(x + 2, 1) || N(x, 1)) = {endpoint_form:.3f}")
|
| 194 |
-
print(f" averaged Girsanov log ratio over {PATHS} paths = {girsanov:.3f}")
|
| 195 |
-
print(f" endpoint mean {float(endpoints.mean()):.3f}, "
|
| 196 |
-
f"variance {float(endpoints.var()):.3f}")
|
| 197 |
-
|
| 198 |
-
value = hopf_cole(bridge_potential) # V_t = -epsilon log h, with h the pinned potential.
|
| 199 |
-
test_points = [(-0.5, 0.2), (0.5, 0.5), (1.5, 0.5), (2.5, 0.8)]
|
| 200 |
-
backward = max(abs(backward_residual(bridge_potential, x, t)) for x, t in test_points)
|
| 201 |
-
hjb = max(abs(hjb_residual(value, x, t)) for x, t in test_points)
|
| 202 |
-
control_gap = max(
|
| 203 |
-
abs(optimal_control(value, x, t)
|
| 204 |
-
- float(bridge_drift(torch.tensor(x, dtype=DTYPE), torch.tensor(t, dtype=DTYPE))))
|
| 205 |
-
for x, t in test_points)
|
| 206 |
-
if loud:
|
| 207 |
-
print(f"\nThe pinned potential h(z, t) and its two equations")
|
| 208 |
-
print(f" largest backward residual d_t h + (eps/2) h'' = {backward:.2e}")
|
| 209 |
-
print(f" largest Hamilton-Jacobi-Bellman residual = {hjb:.2e}")
|
| 210 |
-
print(f" largest gap between -grad V and (y - z) / (1 - t) = {control_gap:.2e}")
|
| 211 |
-
|
| 212 |
-
mean_half, variance_half = bridge_law(0.5)
|
| 213 |
-
sample = simulate_bridge(steps, PATHS, EPSILON, generator)
|
| 214 |
-
half = sample[steps // 2] # The state at t = 1/2 on the grid.
|
| 215 |
-
state = 0.6732 # The particular state Section 7.18 evaluates the drift at.
|
| 216 |
-
drift_here = float(bridge_drift(torch.tensor(state, dtype=DTYPE),
|
| 217 |
-
torch.tensor(0.25, dtype=DTYPE)))
|
| 218 |
-
mean_quarter, variance_quarter = bridge_law(0.25)
|
| 219 |
-
spreads = {e: math.sqrt(e * 0.25) for e in (0.25, 1.0, 4.0)} # Standard deviations at t = 1/2.
|
| 220 |
-
if loud:
|
| 221 |
-
print(f"\nThe bridge from {START:g} to {END:g} at epsilon = {EPSILON:g}")
|
| 222 |
-
print(f" at t = 0.25: mean {mean_quarter:.4f}, variance {variance_quarter:.4f}, "
|
| 223 |
-
f"standard deviation {math.sqrt(variance_quarter):.4f}")
|
| 224 |
-
print(f" at X_0.25 = {state}: drift = ({END:g} - {state}) / 0.75 = {drift_here:.4f}, "
|
| 225 |
-
f"against the straight-line velocity {END - START:.0f}")
|
| 226 |
-
print(f" at t = 1/2: mean {mean_half:.4f}, variance {variance_half:.4f}")
|
| 227 |
-
print(f" simulated mean {float(half.mean()):.4f}, variance {float(half.var()):.4f}")
|
| 228 |
-
print(f" standard deviation at t = 1/2 for eps = 0.25, 1, 4: "
|
| 229 |
-
f"{[round(s, 2) for s in spreads.values()]}")
|
| 230 |
-
|
| 231 |
-
report = {
|
| 232 |
-
"KL from the control energy": closed_form,
|
| 233 |
-
"KL from the two endpoint laws": endpoint_form,
|
| 234 |
-
"KL from the simulated paths": girsanov,
|
| 235 |
-
"simulated endpoint mean": float(endpoints.mean()),
|
| 236 |
-
"backward equation residual": backward,
|
| 237 |
-
"Hamilton-Jacobi-Bellman residual": hjb,
|
| 238 |
-
"-grad V against the bridge drift": control_gap,
|
| 239 |
-
"bridge mean at t = 0.25": mean_quarter,
|
| 240 |
-
"bridge variance at t = 0.25": variance_quarter,
|
| 241 |
-
"bridge drift at X = 0.6732": drift_here,
|
| 242 |
-
"straight-line velocity": float(END - START),
|
| 243 |
-
"bridge mean at t = 1/2": mean_half,
|
| 244 |
-
"bridge variance at t = 1/2": variance_half,
|
| 245 |
-
"simulated mean at t = 1/2": float(half.mean()),
|
| 246 |
-
"simulated variance at t = 1/2": float(half.var()),
|
| 247 |
-
"simulated endpoint, mean": float(sample[-1].mean()),
|
| 248 |
-
"spread at t = 1/2, eps = 0.25": spreads[0.25],
|
| 249 |
-
"spread at t = 1/2, eps = 1": spreads[1.0],
|
| 250 |
-
"spread at t = 1/2, eps = 4": spreads[4.0],
|
| 251 |
-
}
|
| 252 |
-
print_report("A diffusion reference, Chapter 7 Sections 7.16 to 7.18", report)
|
| 253 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
if __name__ == "__main__":
|
| 257 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/optimal_transport.py
DELETED
|
@@ -1,323 +0,0 @@
|
|
| 1 |
-
"""Optimal transport, from a pairing of four points to the Gaussian closed form.
|
| 2 |
-
|
| 3 |
-
Two distributions fix how often each point appears and leave the assignment between them free. This
|
| 4 |
-
file prices every assignment with the squared cost, writes a pairing as a matrix, relaxes Monge's
|
| 5 |
-
map to Kantorovich's coupling, certifies the optimum with prices, and ends with the two Gaussians
|
| 6 |
-
whose optimal map is an affine function.
|
| 7 |
-
|
| 8 |
-
Chapter 7, Sections 7.1 to 7.6 of the notes. Runs on the chapter's own points, with no training and
|
| 9 |
-
no data. Prints the cost of each pairing, the plan and the prices of the two-by-two example, the
|
| 10 |
-
sorted pairing on three points, and the transport of N(0,1) to N(3,4).
|
| 11 |
-
|
| 12 |
-
python lecture_7/optimal_transport.py # every worked number of Sections 7.1 to 7.6
|
| 13 |
-
python lecture_7/optimal_transport.py --steps 20 # a coarse quantile grid, for a first look
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
# %% 01. Imports and configuration
|
| 17 |
-
from __future__ import annotations
|
| 18 |
-
|
| 19 |
-
import itertools
|
| 20 |
-
import math
|
| 21 |
-
|
| 22 |
-
import torch
|
| 23 |
-
|
| 24 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 25 |
-
|
| 26 |
-
DTYPE = torch.float64 # The chapter quotes six figures, so every number here is double precision.
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
# %% 02. The cost of moving one unit of mass
|
| 30 |
-
#
|
| 31 |
-
# The price of carrying one unit from x to y is the squared Euclidean distance, under which a move
|
| 32 |
-
# twice as long costs four times as much. Section 7.1.1.
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def cost_matrix(x: torch.Tensor, y: torch.Tensor, p: float = 2.0) -> torch.Tensor:
|
| 36 |
-
"""C_ij = |x_i - y_j|^p on the line. x: [n], y: [m] -> [n, m]."""
|
| 37 |
-
return (x[:, None] - y[None, :]).abs() ** p # [n, m] one price per route.
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def plan_cost(C: torch.Tensor, pi: torch.Tensor) -> float:
|
| 41 |
-
"""The Frobenius inner product <C, pi>, the mass on each route times the price of that route."""
|
| 42 |
-
return float((C * pi).sum())
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
# %% 03. Four points and two pairings
|
| 46 |
-
#
|
| 47 |
-
# Sources at 0 and 2, targets at 1 and 3, each point with mass 1/2. Section 7.1.2 counts the two
|
| 48 |
-
# arrows of a pairing without weighting them by mass, and Section 7.1.3 weights them.
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def two_pairings() -> dict:
|
| 52 |
-
"""The ordered and crossed pairings of the four points, by arrow count and by mass."""
|
| 53 |
-
x = torch.tensor([0.0, 2.0], dtype=DTYPE) # Source points.
|
| 54 |
-
y = torch.tensor([1.0, 3.0], dtype=DTYPE) # Target points.
|
| 55 |
-
C = cost_matrix(x, y) # [[1, 9], [1, 1]] the four route prices.
|
| 56 |
-
ordered = torch.tensor([[0.5, 0.0], [0.0, 0.5]], dtype=DTYPE) # 0 -> 1 and 2 -> 3.
|
| 57 |
-
crossed = torch.tensor([[0.0, 0.5], [0.5, 0.0]], dtype=DTYPE) # 0 -> 3 and 2 -> 1.
|
| 58 |
-
independent = torch.full((2, 2), 0.25, dtype=DTYPE) # a_i b_j with a = b = (1/2, 1/2).
|
| 59 |
-
return {
|
| 60 |
-
"C": C,
|
| 61 |
-
"arrows_ordered": float(C[0, 0] + C[1, 1]), # 1 + 1, the two arrow lengths squared.
|
| 62 |
-
"arrows_crossed": float(C[0, 1] + C[1, 0]), # 9 + 1.
|
| 63 |
-
"mass_ordered": plan_cost(C, ordered), # Each arrow weighted by the 1/2 it carries.
|
| 64 |
-
"mass_crossed": plan_cost(C, crossed),
|
| 65 |
-
"mass_independent": plan_cost(C, independent), # 1/4 on every route.
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
# %% 04. A pairing written as a matrix
|
| 70 |
-
#
|
| 71 |
-
# A coupling is an n by m matrix with row sums a and column sums b, so pi 1_m = a and pi^T 1_n = b.
|
| 72 |
-
# The independent coupling a_i b_j always lies in that set. Section 7.1.3.
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def is_coupling(pi: torch.Tensor, a: torch.Tensor, b: torch.Tensor, tol: float = 1e-9) -> bool:
|
| 76 |
-
"""True when the row sums equal a, the column sums equal b, and no entry is negative."""
|
| 77 |
-
return bool(
|
| 78 |
-
(pi >= -tol).all()
|
| 79 |
-
and (pi.sum(dim=1) - a).abs().max() < tol # pi 1_m = a.
|
| 80 |
-
and (pi.sum(dim=0) - b).abs().max() < tol # pi^T 1_n = b.
|
| 81 |
-
)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
# %% 05. Monge's formulation, and the source that has to be split
|
| 85 |
-
#
|
| 86 |
-
# A map sends each source point to one destination, so for n equal masses it is a permutation and
|
| 87 |
-
# the minimization runs over n! of them. A point mass pushes forward to a point mass, which is why
|
| 88 |
-
# no map carries delta_0 to a law with mass at two places. Sections 7.2 and 7.2.1.
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def monge_permutations(x: torch.Tensor, y: torch.Tensor) -> list[tuple[float, tuple[int, ...]]]:
|
| 92 |
-
"""Every permutation of n equal point masses and its average cost, cheapest first."""
|
| 93 |
-
n = len(x)
|
| 94 |
-
C = cost_matrix(x, y) # [n, n].
|
| 95 |
-
costs = [(float(sum(C[i, p[i]] for i in range(n)) / n), p)
|
| 96 |
-
for p in itertools.permutations(range(n))] # Mass 1/n on each of the n routes.
|
| 97 |
-
return sorted(costs)
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def pushforward_of_a_point_mass(destination: float) -> dict[float, float]:
|
| 101 |
-
"""T_# delta_0 is the point mass at T(0), whatever the map T does elsewhere."""
|
| 102 |
-
return {destination: 1.0} # One destination, all of the mass, so nu is out of reach.
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
NU_ATOMS = {1.0: 0.5, 2.0: 0.5} # nu = (1/2) delta_1 + (1/2) delta_2 of Section 7.2.1.
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
# %% 06. Kantorovich's relaxation, solved by hand on the two-by-two
|
| 109 |
-
#
|
| 110 |
-
# The feasible set of the chapter's example is the one-parameter family pi(r), and the cost along it
|
| 111 |
-
# is 5.8 - 8r, decreasing, so the optimum sits at the largest feasible r. Sections 7.3 to 7.3.2.
|
| 112 |
-
|
| 113 |
-
SOURCE_X = torch.tensor([0.0, 2.0], dtype=DTYPE) # Source points of the running example.
|
| 114 |
-
SOURCE_A = torch.tensor([0.6, 0.4], dtype=DTYPE) # Their masses.
|
| 115 |
-
TARGET_Y = torch.tensor([1.0, 3.0], dtype=DTYPE) # Target points.
|
| 116 |
-
TARGET_B = torch.tensor([0.3, 0.7], dtype=DTYPE) # Their required masses.
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def feasible_plan(r: float) -> torch.Tensor:
|
| 120 |
-
"""The coupling with mass r from 0 to 1, the one degree of freedom the margins leave."""
|
| 121 |
-
return torch.tensor([[r, 0.6 - r], [0.3 - r, 0.1 + r]], dtype=DTYPE) # [2, 2].
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
def feasible_interval() -> tuple[float, float]:
|
| 125 |
-
"""Nonnegativity of the four entries of pi(r) bounds r, and the binding pair is 0 <= r <= 0.3."""
|
| 126 |
-
lower = max(0.0, -0.1) # From r >= 0 and 0.1 + r >= 0.
|
| 127 |
-
upper = min(0.6, 0.3) # From 0.6 - r >= 0 and 0.3 - r >= 0.
|
| 128 |
-
return lower, upper
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
def cost_along_the_segment(r: float, C: torch.Tensor) -> float:
|
| 132 |
-
"""<C, pi(r)> as a function of r alone, which collects into 5.8 - 8r."""
|
| 133 |
-
return plan_cost(C, feasible_plan(r)) # Linear in r with slope C11 - C12 - C21 + C22 = -8.
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
# %% 07. Prices and the dual
|
| 137 |
-
#
|
| 138 |
-
# A price f_i at each source point and g_j at each target point is feasible when f_i + g_j <= C_ij on
|
| 139 |
-
# every route. Complementary slackness fixes the prices from the loaded routes of an optimal plan,
|
| 140 |
-
# one substitution at a time, after one price is pinned to remove the shift. Sections 7.4 to 7.4.4.
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
def prices_from_loaded_routes(C: torch.Tensor, pi: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
| 144 |
-
"""Solve f_i + g_j = C_ij on the routes carrying mass, with f_1 = 0 fixing the shift."""
|
| 145 |
-
n, m = C.shape
|
| 146 |
-
f = torch.full((n,), float("nan"), dtype=DTYPE)
|
| 147 |
-
g = torch.full((m,), float("nan"), dtype=DTYPE)
|
| 148 |
-
f[0] = 0.0 # Adding a constant to f and subtracting it from g changes no sum f_i + g_j.
|
| 149 |
-
for _ in range(n + m): # Each pass propagates along one more loaded route.
|
| 150 |
-
for i in range(n):
|
| 151 |
-
for j in range(m):
|
| 152 |
-
if pi[i, j] <= 0:
|
| 153 |
-
continue # An empty route imposes no equation.
|
| 154 |
-
if not torch.isnan(f[i]) and torch.isnan(g[j]):
|
| 155 |
-
g[j] = C[i, j] - f[i] # The route prices the target point.
|
| 156 |
-
elif torch.isnan(f[i]) and not torch.isnan(g[j]):
|
| 157 |
-
f[i] = C[i, j] - g[j] # The route prices the source point.
|
| 158 |
-
return f, g
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
def slack(C: torch.Tensor, f: torch.Tensor, g: torch.Tensor) -> torch.Tensor:
|
| 162 |
-
"""The matrix C_ij - f_i - g_j, nonnegative exactly when the prices are feasible."""
|
| 163 |
-
return C - f[:, None] - g[None, :] # [n, m].
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
def dual_value(a: torch.Tensor, f: torch.Tensor, b: torch.Tensor, g: torch.Tensor) -> float:
|
| 167 |
-
"""<a, f> + <b, g>, the value of a pair of prices."""
|
| 168 |
-
return float(a @ f + b @ g)
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
# %% 08. Powers of a distance, and non-crossing on the line
|
| 172 |
-
#
|
| 173 |
-
# With the squared cost the difference between the ordered and crossed pairings is -2 times the
|
| 174 |
-
# product of the two gaps, so an optimal plan on the line contains no crossing. The quantile form
|
| 175 |
-
# then computes W_p by sorting. Sections 7.5 and 7.5.1.
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
def crossing_difference(x1: float, x2: float, y1: float, y2: float) -> tuple[float, float]:
|
| 179 |
-
"""The ordered cost minus the crossed cost, expanded and in the collected form -2 dx dy."""
|
| 180 |
-
expanded = (x1 - y1) ** 2 + (x2 - y2) ** 2 - (x1 - y2) ** 2 - (x2 - y1) ** 2
|
| 181 |
-
collected = -2.0 * (x2 - x1) * (y2 - y1) # Every squared term cancels, leaving the cross terms.
|
| 182 |
-
return expanded, collected
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
def quantile_levels(levels: int) -> torch.Tensor:
|
| 186 |
-
"""Midpoints (i + 1/2) / levels of a uniform grid on [0, 1], the levels q the integral uses."""
|
| 187 |
-
return (torch.arange(levels, dtype=DTYPE) + 0.5) / levels # [levels].
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
def empirical_quantile(points: torch.Tensor, q: torch.Tensor) -> torch.Tensor:
|
| 191 |
-
"""F^{-1}(q) = inf{x : F(x) >= q} for n equally weighted points, a step function of q."""
|
| 192 |
-
ordered = points.sort().values # [n] the sorted points, which are the steps of F^{-1}.
|
| 193 |
-
index = torch.clamp((q * len(points)).floor().long(), max=len(points) - 1) # [levels].
|
| 194 |
-
return ordered[index] # [levels] the quantile at each level.
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
def wasserstein_on_the_line(x: torch.Tensor, y: torch.Tensor, p: float, levels: int) -> float:
|
| 198 |
-
"""W_p^p as the integral over quantile levels q of |F_mu^{-1}(q) - F_nu^{-1}(q)|^p.
|
| 199 |
-
|
| 200 |
-
With n equally weighted points the quantile function is a step function whose values are the
|
| 201 |
-
sorted points, so the level-by-level pairing is the sorted pairing and one sort on each side
|
| 202 |
-
computes the whole integral.
|
| 203 |
-
"""
|
| 204 |
-
q = quantile_levels(levels) # [levels].
|
| 205 |
-
difference = empirical_quantile(x, q) - empirical_quantile(y, q) # [levels].
|
| 206 |
-
return float((difference.abs() ** p).mean()) # The integral of a step function over [0, 1].
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
# %% 09. Gaussians, and the case where the plan is a map
|
| 210 |
-
#
|
| 211 |
-
# For two Gaussians on the line the squared 2-Wasserstein distance is the squared difference of the
|
| 212 |
-
# means plus the squared difference of the standard deviations, and the optimal map is the increasing
|
| 213 |
-
# affine function matching quantiles. Sections 7.6 to 7.6.2.
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
def gaussian_w2_squared(m0: float, s0: float, m1: float, s1: float) -> float:
|
| 217 |
-
"""W_2^2 between N(m0, s0^2) and N(m1, s1^2)."""
|
| 218 |
-
return (m0 - m1) ** 2 + (s0 - s1) ** 2
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
def brenier_map(x: torch.Tensor, m0: float, s0: float, m1: float, s1: float) -> torch.Tensor:
|
| 222 |
-
"""T(x) = m1 + (s1 / s0) (x - m0), the gradient of a convex function in one dimension."""
|
| 223 |
-
return m1 + (s1 / s0) * (x - m0) # Same shape as x.
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
# %% 10. The report
|
| 227 |
-
def main() -> None:
|
| 228 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 229 |
-
parser.set_defaults(steps=3072) # --steps is the number of quantile levels in Section 7.5.1.
|
| 230 |
-
args = parser.parse_args()
|
| 231 |
-
set_seed(args.seed)
|
| 232 |
-
loud = not args.quiet
|
| 233 |
-
|
| 234 |
-
pairings = two_pairings()
|
| 235 |
-
if loud:
|
| 236 |
-
print("Four points: sources at 0 and 2, targets at 1 and 3, mass 1/2 each")
|
| 237 |
-
print(f" arrows ordered {pairings['arrows_ordered']:.0f} "
|
| 238 |
-
f"crossed {pairings['arrows_crossed']:.0f}")
|
| 239 |
-
print(f" by mass ordered {pairings['mass_ordered']:.0f} "
|
| 240 |
-
f"crossed {pairings['mass_crossed']:.0f} "
|
| 241 |
-
f"independent {pairings['mass_independent']:.0f}")
|
| 242 |
-
|
| 243 |
-
C = cost_matrix(SOURCE_X, TARGET_Y) # [[1, 9], [1, 1]] for the running example.
|
| 244 |
-
low, high = feasible_interval()
|
| 245 |
-
grid = torch.linspace(low, high, 7, dtype=DTYPE) # Seven points along the feasible segment.
|
| 246 |
-
best_r = float(grid[int(torch.tensor([cost_along_the_segment(float(r), C)
|
| 247 |
-
for r in grid]).argmin())])
|
| 248 |
-
pi_star = feasible_plan(high) # The cost decreases in r, so the optimum is the right endpoint.
|
| 249 |
-
cost_star = plan_cost(C, pi_star)
|
| 250 |
-
if loud:
|
| 251 |
-
print(f"\nTwo-by-two: a = {SOURCE_A.tolist()}, b = {TARGET_B.tolist()}, C = {C.tolist()}")
|
| 252 |
-
print(f" feasible r in [{low:.1f}, {high:.1f}], cost(r) = "
|
| 253 |
-
f"{cost_along_the_segment(0.0, C):.1f} - "
|
| 254 |
-
f"{(cost_along_the_segment(0.0, C) - cost_along_the_segment(1.0, C)):.0f} r")
|
| 255 |
-
print(f" pi* = {pi_star.tolist()} <C, pi*> = {cost_star:.1f}")
|
| 256 |
-
|
| 257 |
-
f, g = prices_from_loaded_routes(C, pi_star)
|
| 258 |
-
S = slack(C, f, g)
|
| 259 |
-
dual = dual_value(SOURCE_A, f, TARGET_B, g)
|
| 260 |
-
gap = cost_star - dual # Equals sum_ij pi_ij (C_ij - f_i - g_j), term by term nonnegative.
|
| 261 |
-
if loud:
|
| 262 |
-
print(f"\nPrices f = {f.tolist()}, g = {g.tolist()}")
|
| 263 |
-
print(f" slack = {S.tolist()} dual value {dual:.1f} duality gap {gap:.1e}")
|
| 264 |
-
print(f" slackness: mass on a route of positive slack = "
|
| 265 |
-
f"{float((pi_star * (S > 1e-12)).sum()):.1f}")
|
| 266 |
-
|
| 267 |
-
expanded, collected = crossing_difference(0.0, 2.0, 1.0, 3.0)
|
| 268 |
-
three_x = torch.tensor([4.0, 1.0, 6.0], dtype=DTYPE) # Section 7.5.2.
|
| 269 |
-
three_y = torch.tensor([9.0, 2.0, 3.0], dtype=DTYPE)
|
| 270 |
-
permutations = monge_permutations(three_x.sort().values, three_y.sort().values)
|
| 271 |
-
quantile_w22 = wasserstein_on_the_line(three_x, three_y, 2.0, args.steps)
|
| 272 |
-
C3 = cost_matrix(three_x.sort().values, three_y.sort().values)
|
| 273 |
-
f3 = torch.tensor([0.0, 0.0, 8.0], dtype=DTYPE) # Three equations in six unknowns, one solution.
|
| 274 |
-
g3 = torch.tensor([1.0, 1.0, 1.0], dtype=DTYPE)
|
| 275 |
-
dual3 = dual_value(torch.full((3,), 1 / 3, dtype=DTYPE), f3,
|
| 276 |
-
torch.full((3,), 1 / 3, dtype=DTYPE), g3)
|
| 277 |
-
if loud:
|
| 278 |
-
print(f"\nNo crossing: expanded {expanded:+.0f} = collected {collected:+.0f}")
|
| 279 |
-
print(f" three points, six permutations: {[round(c, 3) for c, _ in permutations]}")
|
| 280 |
-
print(f" quantile integral on {args.steps} levels: {quantile_w22:.4f}")
|
| 281 |
-
print(f" slack of f = (0, 0, 8) and g = (1, 1, 1): {slack(C3, f3, g3).tolist()}")
|
| 282 |
-
print(f" dual value {dual3:.4f}")
|
| 283 |
-
|
| 284 |
-
source = torch.special.ndtri(quantile_levels(args.steps)) # N(0,1) read at the same levels.
|
| 285 |
-
mapped = brenier_map(source, 0.0, 1.0, 3.0, 2.0) # T(x) = 3 + 2x.
|
| 286 |
-
displacement_square = float(((mapped - source) ** 2).mean()) # E[(3 + X)^2] = 9 + 1.
|
| 287 |
-
w22_gauss = gaussian_w2_squared(0.0, 1.0, 3.0, 2.0)
|
| 288 |
-
if loud:
|
| 289 |
-
probe = brenier_map(torch.tensor([-1.0, 0.0, 1.0], dtype=DTYPE), 0.0, 1.0, 3.0, 2.0)
|
| 290 |
-
print(f"\nN(0,1) to N(3,4): T(x) = {float(probe[1]):.0f} + "
|
| 291 |
-
f"{float(probe[2] - probe[1]):.0f}x, T(-1) = {float(probe[0]):.0f}, "
|
| 292 |
-
f"T(0) = {float(probe[1]):.0f}, T(1) = {float(probe[2]):.0f}")
|
| 293 |
-
print(f" sampled mean square displacement {displacement_square:.3f}")
|
| 294 |
-
|
| 295 |
-
report = {
|
| 296 |
-
"ordered pairing, by arrows": pairings["arrows_ordered"],
|
| 297 |
-
"crossed pairing, by arrows": pairings["arrows_crossed"],
|
| 298 |
-
"ordered pairing, by mass": pairings["mass_ordered"],
|
| 299 |
-
"crossed pairing, by mass": pairings["mass_crossed"],
|
| 300 |
-
"independent coupling, by mass": pairings["mass_independent"],
|
| 301 |
-
"atoms of T_# delta_0, of nu": f"{len(pushforward_of_a_point_mass(1.0))}, {len(NU_ATOMS)}",
|
| 302 |
-
"permutations at n = 2, 3, 256": f"2, 6, a number with "
|
| 303 |
-
f"{len(str(math.factorial(256)))} digits",
|
| 304 |
-
"argmin of the cost over r": best_r,
|
| 305 |
-
"<C, pi*>": cost_star,
|
| 306 |
-
"dual value <a,f> + <b,g>": dual,
|
| 307 |
-
"duality gap": gap,
|
| 308 |
-
"pi* is a coupling": is_coupling(pi_star, SOURCE_A, TARGET_B),
|
| 309 |
-
"W_2 of the two-by-two": cost_star ** 0.5,
|
| 310 |
-
"sorted pairing on three points": permutations[0][0],
|
| 311 |
-
"worst pairing on three points": permutations[-1][0],
|
| 312 |
-
"W_2^2 from the quantile integral": quantile_w22,
|
| 313 |
-
"dual value on three points": dual3,
|
| 314 |
-
"W_2^2 of N(0,1) and N(3,4)": float(w22_gauss),
|
| 315 |
-
"W_2 of N(0,1) and N(3,4)": float(w22_gauss) ** 0.5,
|
| 316 |
-
"E[(T(X) - X)^2] on a sample": displacement_square,
|
| 317 |
-
}
|
| 318 |
-
print_report("Optimal transport, Chapter 7 Sections 7.1 to 7.6", report)
|
| 319 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
if __name__ == "__main__":
|
| 323 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/ot_flow_matching.py
DELETED
|
@@ -1,215 +0,0 @@
|
|
| 1 |
-
"""Optimal transport inside flow matching: the same loss, trained on a different coupling.
|
| 2 |
-
|
| 3 |
-
The flow matching loss of Chapter 2 is defined for any joint law whose two marginals are p_0 and
|
| 4 |
-
p_1, and the independent draw of a source sample and a data sample is one such law. Replacing it
|
| 5 |
-
with the optimal coupling of the batch changes the training pairs and leaves the loss, the network
|
| 6 |
-
and the sampler as they are. Under the optimal coupling no two paths cross, so the marginal
|
| 7 |
-
velocity at a point is the velocity of the single path through it and the trajectories come out
|
| 8 |
-
straight.
|
| 9 |
-
|
| 10 |
-
Chapter 7, Sections 7.8 and 7.8.1 of the notes. Trains two velocity networks on the four-Gaussian
|
| 11 |
-
target of `cis6270.data`, one on the independent coupling and one on the exact coupling of each
|
| 12 |
-
minibatch. Prints the coupling cost, the straightness of the learned paths and the one-step error.
|
| 13 |
-
|
| 14 |
-
python lecture_7/ot_flow_matching.py # 300 steps per model, under a minute
|
| 15 |
-
python lecture_7/ot_flow_matching.py --steps 20 # a few seconds, for a first look
|
| 16 |
-
"""
|
| 17 |
-
|
| 18 |
-
# %% 01. Imports and configuration
|
| 19 |
-
from __future__ import annotations
|
| 20 |
-
|
| 21 |
-
import torch
|
| 22 |
-
|
| 23 |
-
from cis6270.data import gaussian_mixture_2d
|
| 24 |
-
from cis6270.metrics import mode_report
|
| 25 |
-
from cis6270.nets import VelocityMLP, count_parameters
|
| 26 |
-
from cis6270.runner import (
|
| 27 |
-
common_parser, pick_device, print_report, save_run, set_seed, train,
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
# %% 02. The coupling of a batch
|
| 32 |
-
#
|
| 33 |
-
# A batch holds n source samples and m data samples with uniform masses 1/n and 1/m, and the
|
| 34 |
-
# transport problem on that batch has nm unknowns against n + m equality constraints. With n = m the
|
| 35 |
-
# vertex solution is a permutation, which the Hungarian algorithm returns in order n^3 operations.
|
| 36 |
-
# Section 7.8.1.
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def batch_cost_matrix(x0: torch.Tensor, x1: torch.Tensor) -> torch.Tensor:
|
| 40 |
-
"""C_ij = ||x0_i - x1_j||^2 between every source sample and every data sample. [n, m]."""
|
| 41 |
-
return torch.cdist(x0, x1) ** 2 # [n, m] squared Euclidean cost.
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def optimal_pairing(x0: torch.Tensor, x1: torch.Tensor) -> torch.Tensor:
|
| 45 |
-
"""The permutation of the data samples that minimizes the total squared cost of the batch."""
|
| 46 |
-
from scipy.optimize import linear_sum_assignment # The assignment problem of Section 7.2.
|
| 47 |
-
|
| 48 |
-
C = batch_cost_matrix(x0, x1).detach().cpu().numpy() # [n, n].
|
| 49 |
-
_, columns = linear_sum_assignment(C) # The cheapest permutation, in order n^3 operations.
|
| 50 |
-
return torch.as_tensor(columns, device=x0.device, dtype=torch.long) # [n].
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def couple(x0: torch.Tensor, x1: torch.Tensor, kind: str) -> torch.Tensor:
|
| 54 |
-
"""Reorder the data samples so that row i of the batch is the pair the coupling assigns to x0_i."""
|
| 55 |
-
if kind == "independent":
|
| 56 |
-
return x1 # Two independent draws, which is the coupling of Chapters 2 through 6.
|
| 57 |
-
return x1[optimal_pairing(x0, x1)] # The optimal coupling of this batch.
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def vertex_count(n: int, m: int) -> dict[str, int]:
|
| 61 |
-
"""The size of the batch problem: nm unknowns, n + m constraints, at most n + m - 1 loaded."""
|
| 62 |
-
return {"unknowns": n * m, "constraints": n + m, "loaded_routes": n + m - 1}
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
# %% 03. The loss, with pi the only symbol that changes
|
| 66 |
-
#
|
| 67 |
-
# The network still predicts a velocity, the target is still the straight-line displacement, and the
|
| 68 |
-
# time is still uniform on [0, 1]. Section 7.8.
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def flow_matching_loss(model: VelocityMLP, x0: torch.Tensor, x1: torch.Tensor) -> torch.Tensor:
|
| 72 |
-
"""E_t ||v_theta((1-t) x_0 + t x_1) - (x_1 - x_0)||^2 over the pairs the coupling supplies."""
|
| 73 |
-
t = torch.rand(x0.shape[0], device=x0.device) # [B] one time per pair.
|
| 74 |
-
xt = (1.0 - t[:, None]) * x0 + t[:, None] * x1 # [B, 2] the point on the straight path.
|
| 75 |
-
ut = x1 - x0 # [B, 2] the conditional velocity, constant along the path.
|
| 76 |
-
return ((model(xt, t) - ut) ** 2).sum(dim=1).mean() # Sum coordinates, average pairs.
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def conditional_velocity_energy(x0: torch.Tensor, x1: torch.Tensor) -> float:
|
| 80 |
-
"""The average squared conditional velocity, which Proposition 7.5 makes the kinetic action."""
|
| 81 |
-
return float(((x1 - x0) ** 2).sum(dim=1).mean())
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
# %% 04. Sampling, and two measurements of the learned field
|
| 85 |
-
#
|
| 86 |
-
# A straight trajectory is integrated exactly by one Euler step, so the gap between a one-step
|
| 87 |
-
# sample and a many-step sample measures how far the learned paths are from straight. The second
|
| 88 |
-
# measurement compares the velocity along a trajectory with the net displacement of that
|
| 89 |
-
# trajectory. Section 7.8.
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
@torch.no_grad()
|
| 93 |
-
def integrate(model: VelocityMLP, x0: torch.Tensor, steps: int) -> tuple[torch.Tensor, float]:
|
| 94 |
-
"""Euler integration from t = 0 to t = 1, returning the samples and the path straightness.
|
| 95 |
-
|
| 96 |
-
The straightness is the average of ||v_t(x_t) - (x_1 - x_0)||^2 along the path, which is zero
|
| 97 |
-
exactly when the velocity never changes along a trajectory.
|
| 98 |
-
"""
|
| 99 |
-
x = x0.clone() # [B, 2].
|
| 100 |
-
dt = 1.0 / steps
|
| 101 |
-
velocities = []
|
| 102 |
-
for step in range(steps):
|
| 103 |
-
t = torch.full((x.shape[0],), step * dt, device=x.device) # [B] shared time.
|
| 104 |
-
v = model(x, t) # [B, 2] the learned velocity at the current state.
|
| 105 |
-
velocities.append(v)
|
| 106 |
-
x = x + dt * v # One Euler step.
|
| 107 |
-
net = (x - x0)[None, :, :] # [1, B, 2] the net displacement of each trajectory.
|
| 108 |
-
deviation = ((torch.stack(velocities) - net) ** 2).sum(dim=2).mean() # Scalar.
|
| 109 |
-
return x, float(deviation)
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
@torch.no_grad()
|
| 113 |
-
def one_step_error(model: VelocityMLP, x0: torch.Tensor, steps: int) -> float:
|
| 114 |
-
"""The distance between the one-step sample x_0 + v_0(x_0) and the many-step sample."""
|
| 115 |
-
fine, _ = integrate(model, x0, steps) # [B, 2] the reference trajectory endpoint.
|
| 116 |
-
t0 = torch.zeros(x0.shape[0], device=x0.device)
|
| 117 |
-
coarse = x0 + model(x0, t0) # [B, 2] a single Euler step of size 1.
|
| 118 |
-
return float((coarse - fine).norm(dim=1).mean())
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
# %% 05. Training one model
|
| 122 |
-
def fit(kind: str, args, device: torch.device, data: torch.Tensor) -> dict:
|
| 123 |
-
"""Train a velocity network on the coupling named by ``kind`` and measure what it learned."""
|
| 124 |
-
set_seed(args.seed) # The same initialization and the same batches for both couplings.
|
| 125 |
-
model = VelocityMLP(dim=2, width=args.width).to(device)
|
| 126 |
-
costs: list[float] = []
|
| 127 |
-
|
| 128 |
-
def loss_fn(step: int) -> torch.Tensor:
|
| 129 |
-
x1 = data[torch.randint(0, len(data), (args.batch_size,), device=device)] # [B, 2].
|
| 130 |
-
x0 = torch.randn_like(x1) # [B, 2] the source sample.
|
| 131 |
-
paired = couple(x0, x1, kind) # [B, 2] reordered by the coupling.
|
| 132 |
-
costs.append(conditional_velocity_energy(x0, paired))
|
| 133 |
-
return flow_matching_loss(model, x0, paired)
|
| 134 |
-
|
| 135 |
-
losses = train(model, loss_fn, steps=args.steps, lr=args.lr, quiet=True)
|
| 136 |
-
torch.manual_seed(args.seed + 1) # The same starting noise for both samplers.
|
| 137 |
-
x0 = torch.randn(args.samples, 2, device=device)
|
| 138 |
-
samples, straightness = integrate(model, x0, args.sample_steps)
|
| 139 |
-
report = mode_report(samples.cpu())
|
| 140 |
-
return {
|
| 141 |
-
"kind": kind,
|
| 142 |
-
"loss": sum(losses[-50:]) / len(losses[-50:]),
|
| 143 |
-
"coupling_cost": sum(costs) / len(costs),
|
| 144 |
-
"straightness": straightness,
|
| 145 |
-
"one_step_error": one_step_error(model, x0, args.sample_steps),
|
| 146 |
-
"modes_found": report["modes_found"],
|
| 147 |
-
"nearest_distance": report["nearest_distance"],
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
# %% 06. The report
|
| 152 |
-
def main() -> None:
|
| 153 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 154 |
-
parser.set_defaults(steps=300, batch_size=128, width=64, samples=512, sample_steps=50)
|
| 155 |
-
args = parser.parse_args()
|
| 156 |
-
set_seed(args.seed)
|
| 157 |
-
device = pick_device(args.device)
|
| 158 |
-
loud = not args.quiet
|
| 159 |
-
data = gaussian_mixture_2d(num_points=4096, seed=args.seed).to(device) # [4096, 2].
|
| 160 |
-
|
| 161 |
-
# The two-point batch of Section 7.8.1, where both couplings can be written out.
|
| 162 |
-
x = torch.tensor([[0.0], [2.0]]) # Two source samples on the line.
|
| 163 |
-
y = torch.tensor([[1.0], [3.0]]) # Two data samples.
|
| 164 |
-
C = batch_cost_matrix(x, y) # [[1, 9], [1, 1]].
|
| 165 |
-
independent_energy = float(C.mean()) # Weight 1/4 on each of the four pairs.
|
| 166 |
-
optimal_energy = float(C[torch.arange(2), optimal_pairing(x, y)].mean()) # Weight 1/2 each.
|
| 167 |
-
sizes = vertex_count(256, 256) # The counting the chapter does, at n = m = 256.
|
| 168 |
-
if loud:
|
| 169 |
-
print("Two source samples at 0 and 2, two data samples at 1 and 3")
|
| 170 |
-
print(f" independent coupling, average squared velocity {independent_energy:.0f}")
|
| 171 |
-
print(f" optimal coupling, average squared velocity {optimal_energy:.0f}")
|
| 172 |
-
print(f"\nA batch of 256 and 256: {sizes['unknowns']:,} unknowns, "
|
| 173 |
-
f"{sizes['constraints']} constraints, at most {sizes['loaded_routes']} loaded routes")
|
| 174 |
-
print(f" an exact solve costs about {256 ** 3:.3g} operations, repeated every batch")
|
| 175 |
-
print(f" training batch {args.batch_size}, device={device}, parameters="
|
| 176 |
-
f"{count_parameters(VelocityMLP(dim=2, width=args.width)):,}")
|
| 177 |
-
|
| 178 |
-
results = {}
|
| 179 |
-
for kind in ("independent", "optimal"):
|
| 180 |
-
if loud:
|
| 181 |
-
print(f"\ntraining on the {kind} coupling")
|
| 182 |
-
results[kind] = fit(kind, args, device, data)
|
| 183 |
-
if loud:
|
| 184 |
-
r = results[kind]
|
| 185 |
-
print(f" loss {r['loss']:.4f} batch coupling cost {r['coupling_cost']:.3f}")
|
| 186 |
-
print(f" straightness {r['straightness']:.4f} "
|
| 187 |
-
f"one-step error {r['one_step_error']:.4f}")
|
| 188 |
-
|
| 189 |
-
ratio = results["independent"]["coupling_cost"] / max(results["optimal"]["coupling_cost"], 1e-12)
|
| 190 |
-
report = {
|
| 191 |
-
"two-point batch, independent": independent_energy,
|
| 192 |
-
"two-point batch, optimal": optimal_energy,
|
| 193 |
-
"batch plan unknowns": sizes["unknowns"],
|
| 194 |
-
"batch plan constraints": sizes["constraints"],
|
| 195 |
-
"loaded routes at a vertex": sizes["loaded_routes"],
|
| 196 |
-
"coupling cost, independent": results["independent"]["coupling_cost"],
|
| 197 |
-
"coupling cost, optimal": results["optimal"]["coupling_cost"],
|
| 198 |
-
"cost ratio": ratio,
|
| 199 |
-
"training loss, independent": results["independent"]["loss"],
|
| 200 |
-
"training loss, optimal": results["optimal"]["loss"],
|
| 201 |
-
"straightness, independent": results["independent"]["straightness"],
|
| 202 |
-
"straightness, optimal": results["optimal"]["straightness"],
|
| 203 |
-
"one-step error, independent": results["independent"]["one_step_error"],
|
| 204 |
-
"one-step error, optimal": results["optimal"]["one_step_error"],
|
| 205 |
-
"modes found, independent": results["independent"]["modes_found"],
|
| 206 |
-
"modes found, optimal": results["optimal"]["modes_found"],
|
| 207 |
-
"nearest mode, independent": results["independent"]["nearest_distance"],
|
| 208 |
-
"nearest mode, optimal": results["optimal"]["nearest_distance"],
|
| 209 |
-
}
|
| 210 |
-
print_report("Optimal transport inside flow matching, Chapter 7 Section 7.8", report)
|
| 211 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
if __name__ == "__main__":
|
| 215 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/sf2m.py
DELETED
|
@@ -1,248 +0,0 @@
|
|
| 1 |
-
"""Simulation-Free Score and Flow Matching: two closed-form targets at one bridge sample.
|
| 2 |
-
|
| 3 |
-
With the entropic plan computed on a minibatch, the bridge is a mixture of Brownian bridges between
|
| 4 |
-
pairs drawn from that plan, and a Brownian bridge at a time t is Gaussian. One normal draw gives a
|
| 5 |
-
sample, and at that sample both the bridge drift and the score of the bridge marginal are available
|
| 6 |
-
without simulating a path. Subtracting half the noise variance times the score turns the drift into
|
| 7 |
-
the velocity of the probability flow, whose dependence on epsilon cancels.
|
| 8 |
-
|
| 9 |
-
Chapter 7, Section 7.19.3 of the notes. Runs on the endpoint pair x_0 = 0, x_1 = 2 at epsilon = 1,
|
| 10 |
-
and on the two-point batch of Section 7.8.1. Prints the two regression targets at t = 0.25 and at
|
| 11 |
-
t = 0.75, the scaled score at every time on the grid, and the entropic plan of the batch.
|
| 12 |
-
|
| 13 |
-
python lecture_7/sf2m.py # 101 times on the grid
|
| 14 |
-
python lecture_7/sf2m.py --steps 20 # a coarse time grid, for a first look
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
# %% 01. Imports and configuration
|
| 18 |
-
from __future__ import annotations
|
| 19 |
-
|
| 20 |
-
import torch
|
| 21 |
-
|
| 22 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 23 |
-
|
| 24 |
-
DTYPE = torch.float64 # The chapter quotes four figures of both regression targets.
|
| 25 |
-
|
| 26 |
-
X0, X1 = 0.0, 2.0 # The endpoint pair the chapter works through.
|
| 27 |
-
EPSILON = 1.0 # The reference noise variance per unit time.
|
| 28 |
-
DRAW = 0.4 # The normal draw z the chapter uses at both times.
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
# %% 02. One sample from the bridge
|
| 32 |
-
#
|
| 33 |
-
# The Brownian bridge at time t is Gaussian with mean the straight interpolation and variance
|
| 34 |
-
# epsilon t (1 - t), so a sample costs one normal draw and no intermediate step.
|
| 35 |
-
# Section 7.19.3.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def bridge_mean(x0: torch.Tensor, x1: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 39 |
-
"""m_t = (1 - t) x_0 + t x_1, the mean of the bridge between the two ends."""
|
| 40 |
-
return (1.0 - t) * x0 + t * x1
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def bridge_std(t: torch.Tensor, epsilon: float = EPSILON) -> torch.Tensor:
|
| 44 |
-
"""sigma_t = sqrt(epsilon t (1 - t)), which vanishes at both ends and peaks at t = 1/2."""
|
| 45 |
-
return torch.sqrt(epsilon * t * (1.0 - t))
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def bridge_sample(x0: torch.Tensor, x1: torch.Tensor, t: torch.Tensor, z: torch.Tensor,
|
| 49 |
-
epsilon: float = EPSILON) -> torch.Tensor:
|
| 50 |
-
"""x = m_t + sigma_t z, one draw from the bridge at time t."""
|
| 51 |
-
return bridge_mean(x0, x1, t) + bridge_std(t, epsilon) * z
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
# %% 03. The two conditional quantities
|
| 55 |
-
#
|
| 56 |
-
# Both are conditional on the drawn pair, and neither depends on a quantity a network has to
|
| 57 |
-
# estimate first. Section 7.19.3.
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def conditional_drift(x1: torch.Tensor, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 61 |
-
"""u_t(x | x_0, x_1) = (x_1 - x) / (1 - t), the remaining displacement over the remaining time."""
|
| 62 |
-
return (x1 - x) / (1.0 - t)
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def conditional_score(x0: torch.Tensor, x1: torch.Tensor, x: torch.Tensor, t: torch.Tensor,
|
| 66 |
-
epsilon: float = EPSILON) -> torch.Tensor:
|
| 67 |
-
"""grad log p_t(x | x_0, x_1) = (m_t - x) / (epsilon t (1 - t)), displacement over variance."""
|
| 68 |
-
return (bridge_mean(x0, x1, t) - x) / (epsilon * t * (1.0 - t))
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
# %% 04. From the bridge drift to a probability flow velocity
|
| 72 |
-
#
|
| 73 |
-
# Subtracting half the noise variance times the score converts a diffusion drift into the velocity
|
| 74 |
-
# of the probability flow, which has the same marginals with no noise at all. The second form shows
|
| 75 |
-
# the straight-line velocity plus a coefficient times the displacement of the sample from the mean,
|
| 76 |
-
# with the dependence on epsilon cancelled. Section 7.19.3.
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def flow_coefficient(t: torch.Tensor) -> torch.Tensor:
|
| 80 |
-
"""(1 - 2t) / (2 t (1 - t)), positive before t = 1/2 and negative after."""
|
| 81 |
-
return (1.0 - 2.0 * t) / (2.0 * t * (1.0 - t))
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def flow_velocity(x0: torch.Tensor, x1: torch.Tensor, x: torch.Tensor, t: torch.Tensor,
|
| 85 |
-
epsilon: float = EPSILON) -> torch.Tensor:
|
| 86 |
-
"""u_t - (epsilon / 2) grad log p_t, the velocity the first network regresses onto."""
|
| 87 |
-
return (conditional_drift(x1, x, t)
|
| 88 |
-
- 0.5 * epsilon * conditional_score(x0, x1, x, t, epsilon))
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def flow_velocity_collected(x0: torch.Tensor, x1: torch.Tensor, x: torch.Tensor,
|
| 92 |
-
t: torch.Tensor) -> torch.Tensor:
|
| 93 |
-
"""(x_1 - x_0) + (1 - 2t) / (2 t (1 - t)) (x - m_t), the same velocity with delta collected."""
|
| 94 |
-
return (x1 - x0) + flow_coefficient(t) * (x - bridge_mean(x0, x1, t))
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def scaled_score(x0: torch.Tensor, x1: torch.Tensor, x: torch.Tensor, t: torch.Tensor,
|
| 98 |
-
epsilon: float = EPSILON) -> torch.Tensor:
|
| 99 |
-
"""sigma_t grad log p_t = (m_t - x) / sigma_t = -z, which has unit scale at every time."""
|
| 100 |
-
return bridge_std(t, epsilon) * conditional_score(x0, x1, x, t, epsilon)
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
# %% 05. The loss and the sampler
|
| 104 |
-
#
|
| 105 |
-
# With lambda(t) = sigma_t the score target is -z, so no part of the schedule dominates the
|
| 106 |
-
# gradient. At sampling time a noise level g(t) is fixed and the two networks are integrated
|
| 107 |
-
# together, with g^2 = epsilon recovering the reference noise and g = 0 the probability flow.
|
| 108 |
-
# Section 7.19.3.
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def sf2m_loss(velocity: torch.Tensor, score: torch.Tensor, velocity_target: torch.Tensor,
|
| 112 |
-
score_target: torch.Tensor, t: torch.Tensor, epsilon: float = EPSILON
|
| 113 |
-
) -> torch.Tensor:
|
| 114 |
-
"""||v_theta - u^o_t||^2 + lambda(t)^2 ||s_theta - grad log p_t||^2, with lambda(t) = sigma_t."""
|
| 115 |
-
weight = bridge_std(t, epsilon) ** 2 # lambda(t)^2, which turns the score target into -z.
|
| 116 |
-
return ((velocity - velocity_target) ** 2 + weight * (score - score_target) ** 2).mean()
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def sampler_drift(velocity: torch.Tensor, score: torch.Tensor, noise_level: float
|
| 120 |
-
) -> torch.Tensor:
|
| 121 |
-
"""v_theta + (1/2) g(t)^2 s_theta, the drift of the family of samplers between the two ends."""
|
| 122 |
-
return velocity + 0.5 * noise_level ** 2 * score
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
# %% 06. The role of the coupling
|
| 126 |
-
#
|
| 127 |
-
# Pairs from the entropic plan at strength 2 epsilon make the learned equation at g^2 = epsilon the
|
| 128 |
-
# Schrodinger bridge, and pairs from any other coupling make it the Markov projection of that
|
| 129 |
-
# mixture of bridges. The plan is computed on a minibatch. Section 7.19.3.
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
def entropic_plan(x: torch.Tensor, y: torch.Tensor, a: torch.Tensor, b: torch.Tensor,
|
| 133 |
-
epsilon: float = EPSILON, sweeps: int = 200) -> torch.Tensor:
|
| 134 |
-
"""The entropic plan for the squared cost at strength 2 epsilon, by Sinkhorn on exp(-C / 2 eps)."""
|
| 135 |
-
C = (x[:, None] - y[None, :]) ** 2 # [n, m] the squared cost of the batch.
|
| 136 |
-
K = torch.exp(-C / (2.0 * epsilon)) # The Gibbs kernel a Brownian reference produces.
|
| 137 |
-
u, v = torch.ones_like(a), torch.ones_like(b)
|
| 138 |
-
for _ in range(sweeps):
|
| 139 |
-
u = a / (K @ v)
|
| 140 |
-
v = b / (K.T @ u)
|
| 141 |
-
return u[:, None] * K * v[None, :] # [n, m].
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
# %% 07. The report
|
| 145 |
-
def main() -> None:
|
| 146 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 147 |
-
parser.set_defaults(steps=101) # --steps is the number of times on the grid.
|
| 148 |
-
args = parser.parse_args()
|
| 149 |
-
set_seed(args.seed)
|
| 150 |
-
loud = not args.quiet
|
| 151 |
-
x0 = torch.tensor(X0, dtype=DTYPE)
|
| 152 |
-
x1 = torch.tensor(X1, dtype=DTYPE)
|
| 153 |
-
z = torch.tensor(DRAW, dtype=DTYPE)
|
| 154 |
-
|
| 155 |
-
worked = {}
|
| 156 |
-
for value in (0.25, 0.75):
|
| 157 |
-
t = torch.tensor(value, dtype=DTYPE)
|
| 158 |
-
x = bridge_sample(x0, x1, t, z)
|
| 159 |
-
worked[value] = {
|
| 160 |
-
"mean": float(bridge_mean(x0, x1, t)),
|
| 161 |
-
"std": float(bridge_std(t)),
|
| 162 |
-
"sample": float(x),
|
| 163 |
-
"drift": float(conditional_drift(x1, x, t)),
|
| 164 |
-
"score": float(conditional_score(x0, x1, x, t)),
|
| 165 |
-
"flow": float(flow_velocity(x0, x1, x, t)),
|
| 166 |
-
"collected": float(flow_velocity_collected(x0, x1, x, t)),
|
| 167 |
-
"scaled": float(scaled_score(x0, x1, x, t)),
|
| 168 |
-
"coefficient": float(flow_coefficient(t)),
|
| 169 |
-
}
|
| 170 |
-
if loud:
|
| 171 |
-
w = worked[value]
|
| 172 |
-
print(f"t = {value}, z = {DRAW}")
|
| 173 |
-
print(f" m_t = {w['mean']:.4f} sigma_t = {w['std']:.4f} x = {w['sample']:.4f}")
|
| 174 |
-
print(f" u_t = {w['drift']:.4f} grad log p_t = {w['score']:.4f}")
|
| 175 |
-
print(f" u^o_t = {w['flow']:.4f} collected form {w['collected']:.4f} "
|
| 176 |
-
f"coefficient {w['coefficient']:+.4f}")
|
| 177 |
-
print(f" sigma_t grad log p_t = {w['scaled']:.4f}, which is -z")
|
| 178 |
-
|
| 179 |
-
grid = torch.linspace(0.0, 1.0, max(args.steps, 3), dtype=DTYPE)[1:-1] # Interior times.
|
| 180 |
-
samples = bridge_sample(x0, x1, grid, z) # [levels] one bridge sample per time.
|
| 181 |
-
scaled = scaled_score(x0, x1, samples, grid)
|
| 182 |
-
scaled_gap = float((scaled + z).abs().max()) # The target is -z at every time.
|
| 183 |
-
flow_targets = flow_velocity(x0, x1, samples, grid)
|
| 184 |
-
score_targets = conditional_score(x0, x1, samples, grid)
|
| 185 |
-
loss_exact = float(sf2m_loss(flow_targets, score_targets, flow_targets, score_targets, grid))
|
| 186 |
-
loss_zero = float(sf2m_loss(torch.zeros_like(flow_targets), torch.zeros_like(score_targets),
|
| 187 |
-
flow_targets, score_targets, grid)) # An untrained pair of networks.
|
| 188 |
-
stratified = torch.special.ndtri(
|
| 189 |
-
(torch.arange(max(args.steps, 3), dtype=DTYPE) + 0.5) / max(args.steps, 3))
|
| 190 |
-
half = torch.tensor(0.5, dtype=DTYPE)
|
| 191 |
-
averaged = float(flow_velocity(x0, x1, bridge_sample(x0, x1, half, stratified), half).mean())
|
| 192 |
-
symmetry = worked[0.25]["flow"] + worked[0.75]["flow"] - 2.0 * (X1 - X0)
|
| 193 |
-
if loud:
|
| 194 |
-
print(f"\nOn {len(grid)} interior times")
|
| 195 |
-
print(f" largest gap between sigma_t grad log p_t and -z: {scaled_gap:.2e}")
|
| 196 |
-
print(f" u^o averaged over the draw at t = 1/2: {averaged:.4f}, "
|
| 197 |
-
f"against the straight-line velocity {X1 - X0:.0f}")
|
| 198 |
-
print(f" the two flow targets about the straight-line velocity: {symmetry:.2e}")
|
| 199 |
-
print(f" loss at the exact targets {loss_exact:.2e}, at a zero network {loss_zero:.4f}")
|
| 200 |
-
|
| 201 |
-
x = torch.tensor([0.0, 2.0], dtype=DTYPE) # The batch of Section 7.8.1.
|
| 202 |
-
y = torch.tensor([1.0, 3.0], dtype=DTYPE)
|
| 203 |
-
uniform = torch.full((2,), 0.5, dtype=DTYPE)
|
| 204 |
-
C = (x[:, None] - y[None, :]) ** 2
|
| 205 |
-
plan = entropic_plan(x, y, uniform, uniform)
|
| 206 |
-
ordered_cost = float(C[0, 0] + C[1, 1]) # 1 + 1, the arrows of the exact pairing.
|
| 207 |
-
crossed_cost = float(C[0, 1] + C[1, 0]) # 9 + 1, the arrows of the crossed pairing.
|
| 208 |
-
if loud:
|
| 209 |
-
print(f"\nThe two-point batch at epsilon = {EPSILON:g}")
|
| 210 |
-
print(f" exact transport pairs 0 with 1 and 2 with 3 at cost {ordered_cost:.0f}, "
|
| 211 |
-
f"against {crossed_cost:.0f} for the crossed pairing")
|
| 212 |
-
print(f" entropic plan at strength {2 * EPSILON:g}: "
|
| 213 |
-
f"{[[round(float(z_), 6) for z_ in row] for row in plan]}")
|
| 214 |
-
print(f" mass on the crossed pairing {float(plan[0, 1] + plan[1, 0]):.6f}")
|
| 215 |
-
|
| 216 |
-
report = {
|
| 217 |
-
"m_t at t = 0.25": worked[0.25]["mean"],
|
| 218 |
-
"sigma_t at t = 0.25": worked[0.25]["std"],
|
| 219 |
-
"bridge sample at t = 0.25": worked[0.25]["sample"],
|
| 220 |
-
"u_t at t = 0.25": worked[0.25]["drift"],
|
| 221 |
-
"grad log p_t at t = 0.25": worked[0.25]["score"],
|
| 222 |
-
"u^o_t at t = 0.25": worked[0.25]["flow"],
|
| 223 |
-
"coefficient at t = 0.25": worked[0.25]["coefficient"],
|
| 224 |
-
"sigma_t grad log p_t at t = 0.25": worked[0.25]["scaled"],
|
| 225 |
-
"bridge sample at t = 0.75": worked[0.75]["sample"],
|
| 226 |
-
"u_t at t = 0.75": worked[0.75]["drift"],
|
| 227 |
-
"u^o_t at t = 0.75": worked[0.75]["flow"],
|
| 228 |
-
"coefficient at t = 0.75": worked[0.75]["coefficient"],
|
| 229 |
-
"the two targets, sum minus 2 u": symmetry,
|
| 230 |
-
"largest gap of the scaled score": scaled_gap,
|
| 231 |
-
"u^o averaged over the draw": averaged,
|
| 232 |
-
"loss at the exact targets": loss_exact,
|
| 233 |
-
"loss at a zero network": loss_zero,
|
| 234 |
-
"sampler drift at g^2 = epsilon": float(sampler_drift(
|
| 235 |
-
torch.tensor(worked[0.25]["flow"], dtype=DTYPE),
|
| 236 |
-
torch.tensor(worked[0.25]["score"], dtype=DTYPE), EPSILON ** 0.5)),
|
| 237 |
-
"sampler drift at g = 0": worked[0.25]["flow"],
|
| 238 |
-
"ordered pairing of the batch": ordered_cost,
|
| 239 |
-
"crossed pairing of the batch": crossed_cost,
|
| 240 |
-
"entropic plan, ordered mass": float(plan[0, 0] + plan[1, 1]),
|
| 241 |
-
"entropic plan, crossed mass": float(plan[0, 1] + plan[1, 0]),
|
| 242 |
-
}
|
| 243 |
-
print_report("Simulation-Free Score and Flow Matching, Chapter 7 Section 7.19.3", report)
|
| 244 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
if __name__ == "__main__":
|
| 248 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/sinkhorn.py
DELETED
|
@@ -1,234 +0,0 @@
|
|
| 1 |
-
"""Entropic transport and Sinkhorn's algorithm on the chapter's two-by-two example.
|
| 2 |
-
|
| 3 |
-
Subtracting epsilon times the entropy from the transport cost makes the objective strictly convex,
|
| 4 |
-
and its minimizer is the Gibbs kernel exp(-C/epsilon) with one scaling per row and one per column.
|
| 5 |
-
The two marginal constraints determine the scalings by alternate rescaling, which is one relative
|
| 6 |
-
entropy projection per half-step.
|
| 7 |
-
|
| 8 |
-
Chapter 7, Sections 7.9 and 7.10 of the notes. Runs on a = (0.6, 0.4), b = (0.3, 0.7) and the cost
|
| 9 |
-
matrix with entries 1, 9, 1, 1. Prints the kernel, the first sweep by hand, the marginal error after
|
| 10 |
-
each sweep, the converged plan and its multipliers, and the plan at four values of epsilon.
|
| 11 |
-
|
| 12 |
-
python lecture_7/sinkhorn.py # 40 sweeps, every worked number of Section 7.10
|
| 13 |
-
python lecture_7/sinkhorn.py --steps 1 # the first sweep alone, computed by hand
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
# %% 01. Imports and configuration
|
| 17 |
-
from __future__ import annotations
|
| 18 |
-
|
| 19 |
-
import torch
|
| 20 |
-
|
| 21 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 22 |
-
|
| 23 |
-
DTYPE = torch.float64 # The chapter quotes six figures of the kernel and of the converged plan.
|
| 24 |
-
|
| 25 |
-
COST = torch.tensor([[1.0, 9.0], [1.0, 1.0]], dtype=DTYPE) # C of Section 7.3.1.
|
| 26 |
-
SOURCE_A = torch.tensor([0.6, 0.4], dtype=DTYPE) # Row sums.
|
| 27 |
-
TARGET_B = torch.tensor([0.3, 0.7], dtype=DTYPE) # Column sums.
|
| 28 |
-
EXACT_PLAN = torch.tensor([[0.3, 0.3], [0.0, 0.4]], dtype=DTYPE) # pi* of Section 7.3.2.
|
| 29 |
-
EPSILON = 2.0 # The strength the chapter runs by hand.
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
# %% 02. Entropy of a plan, and the entropic objective
|
| 33 |
-
#
|
| 34 |
-
# Entropy is larger when the mass spreads over many routes and smaller when it concentrates, with
|
| 35 |
-
# the convention 0 log 0 = 0. Subtracting epsilon H(pi) from the cost makes the objective strictly
|
| 36 |
-
# convex, so the minimizer is unique and every entry of it is positive. Section 7.9.
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def entropy(pi: torch.Tensor) -> float:
|
| 40 |
-
"""H(pi) = -sum_ij pi_ij log pi_ij, in nats, with an empty route contributing nothing."""
|
| 41 |
-
positive = pi[pi > 0] # Drop the zero entries, which contribute 0 log 0 = 0.
|
| 42 |
-
return float(-(positive * positive.log()).sum())
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def entropic_objective(pi: torch.Tensor, C: torch.Tensor, epsilon: float) -> float:
|
| 46 |
-
"""<C, pi> - epsilon H(pi), the quantity the entropic problem minimizes over Pi(a, b)."""
|
| 47 |
-
return float((C * pi).sum()) - epsilon * entropy(pi)
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
# %% 03. Relative entropy from a positive matrix
|
| 51 |
-
#
|
| 52 |
-
# The definition keeps its meaning when xi is any strictly positive matrix. Taking xi to be the
|
| 53 |
-
# independent coupling splits the logarithm into three pieces and gives -H(pi) + H(a) + H(b), so
|
| 54 |
-
# the entropy term and a relative entropy from independence differ by a constant on Pi(a, b).
|
| 55 |
-
# Section 7.9.1.
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
def relative_entropy(pi: torch.Tensor, xi: torch.Tensor) -> float:
|
| 59 |
-
"""KL(pi || xi) = sum_ij pi_ij log(pi_ij / xi_ij) for a strictly positive xi."""
|
| 60 |
-
mask = pi > 0 # Routes with no mass contribute nothing.
|
| 61 |
-
return float((pi[mask] * (pi[mask] / xi[mask]).log()).sum())
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def independent_coupling(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
| 65 |
-
"""a_i b_j, the coupling of largest entropy and the one Chapters 2 through 6 used."""
|
| 66 |
-
return a[:, None] * b[None, :] # [n, m].
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
# %% 04. The Lagrangian and the scaling form
|
| 70 |
-
#
|
| 71 |
-
# Setting the derivative of the Lagrangian in a single entry to zero gives
|
| 72 |
-
# C_ij + epsilon (log pi_ij + 1) - f_i - g_j = 0, and solving for the logarithm splits the
|
| 73 |
-
# minimizer into a row factor, a kernel entry and a column factor. Section 7.9.2.
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def gibbs_kernel(C: torch.Tensor, epsilon: float) -> torch.Tensor:
|
| 77 |
-
"""K = exp(-C / epsilon), taken entrywise, with larger entries on the cheaper routes."""
|
| 78 |
-
return torch.exp(-C / epsilon) # [n, m], every entry strictly positive.
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def scaling_form(u: torch.Tensor, K: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
|
| 82 |
-
"""diag(u) K diag(v), the plan with row i scaled by u_i and column j scaled by v_j."""
|
| 83 |
-
return u[:, None] * K * v[None, :] # [n, m].
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def multipliers_from_scalings(u: torch.Tensor, epsilon: float) -> torch.Tensor:
|
| 87 |
-
"""Invert u_i = exp(f_i / epsilon - 1/2) for f_i = epsilon (log u_i + 1/2)."""
|
| 88 |
-
return epsilon * (u.log() + 0.5) # [n] the additive price behind a multiplicative scaling.
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
# %% 05. The two updates
|
| 92 |
-
#
|
| 93 |
-
# The row sums of diag(u) K diag(v) are u * (K v) and the column sums are v * (K^T u), and solving
|
| 94 |
-
# each equation for its own scaling with the other held fixed gives the two updates. One sweep is
|
| 95 |
-
# the pair of them, at two matrix-vector products. Section 7.10.
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
def sinkhorn_sweeps(
|
| 99 |
-
K: torch.Tensor, a: torch.Tensor, b: torch.Tensor, sweeps: int,
|
| 100 |
-
) -> tuple[torch.Tensor, torch.Tensor, list[float]]:
|
| 101 |
-
"""Alternate u <- a / (K v) and v <- b / (K^T u), returning the scalings and the sweep errors."""
|
| 102 |
-
u = torch.ones_like(a)
|
| 103 |
-
v = torch.ones_like(b)
|
| 104 |
-
errors: list[float] = []
|
| 105 |
-
for _ in range(sweeps):
|
| 106 |
-
u = a / (K @ v) # Row sums become exactly a.
|
| 107 |
-
v = b / (K.T @ u) # Column sums become exactly b, and the row sums move away again.
|
| 108 |
-
errors.append(marginal_error(scaling_form(u, K, v), a, b))
|
| 109 |
-
return u, v, errors
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def marginal_error(pi: torch.Tensor, a: torch.Tensor, b: torch.Tensor) -> float:
|
| 113 |
-
"""Total absolute error of the row sums against a plus that of the column sums against b."""
|
| 114 |
-
return float((pi.sum(dim=1) - a).abs().sum() + (pi.sum(dim=0) - b).abs().sum())
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
def sweeps_to_tolerance(K: torch.Tensor, a: torch.Tensor, b: torch.Tensor,
|
| 118 |
-
tolerance: float = 1e-9, limit: int = 2000) -> int:
|
| 119 |
-
"""The number of sweeps that brings the total marginal error below the tolerance."""
|
| 120 |
-
_, _, errors = sinkhorn_sweeps(K, a, b, limit)
|
| 121 |
-
for index, error in enumerate(errors):
|
| 122 |
-
if error < tolerance:
|
| 123 |
-
return index + 1
|
| 124 |
-
return limit
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
# %% 06. Each update as a relative entropy projection
|
| 128 |
-
#
|
| 129 |
-
# Minimizing KL(pi || xi) over the matrices with row sums a, with no condition on the columns, gives
|
| 130 |
-
# pi_ij = xi_ij exp(lambda_i - 1), which is xi with each row rescaled, and the row sums fix the
|
| 131 |
-
# factor at a_i / sum_j xi_ij. The u update performs exactly that rescaling. Section 7.10.1.
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
def project_rows(xi: torch.Tensor, a: torch.Tensor) -> torch.Tensor:
|
| 135 |
-
"""The matrix with row sums a closest to xi in relative entropy."""
|
| 136 |
-
return xi * (a / xi.sum(dim=1))[:, None] # One factor per row.
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
def project_columns(xi: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
|
| 140 |
-
"""The matrix with column sums b closest to xi in relative entropy."""
|
| 141 |
-
return xi * (b / xi.sum(dim=0))[None, :] # One factor per column.
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
# %% 07. The report
|
| 145 |
-
def main() -> None:
|
| 146 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 147 |
-
parser.add_argument("--epsilon", type=float, default=EPSILON, help="regularization strength")
|
| 148 |
-
parser.set_defaults(steps=40) # --steps is the number of Sinkhorn sweeps.
|
| 149 |
-
args = parser.parse_args()
|
| 150 |
-
set_seed(args.seed)
|
| 151 |
-
loud = not args.quiet
|
| 152 |
-
epsilon = args.epsilon
|
| 153 |
-
|
| 154 |
-
K = gibbs_kernel(COST, epsilon)
|
| 155 |
-
independent = independent_coupling(SOURCE_A, TARGET_B)
|
| 156 |
-
if loud:
|
| 157 |
-
print(f"H(pi*) = {entropy(EXACT_PLAN):.3f} H(a x b) = {entropy(independent):.3f} "
|
| 158 |
-
f"H(a) + H(b) = {entropy(SOURCE_A) + entropy(TARGET_B):.3f}")
|
| 159 |
-
print(f"KL(pi* || a x b) = {relative_entropy(EXACT_PLAN, independent):.3f} nats")
|
| 160 |
-
print(f"\nK = exp(-C/{epsilon:g}) = {[[round(float(k), 6) for k in row] for row in K]}")
|
| 161 |
-
print(f" kernel ratio between a cheap route and the expensive one "
|
| 162 |
-
f"{float(K[0, 0] / K[0, 1]):.1f}")
|
| 163 |
-
|
| 164 |
-
# The first sweep, written out the way Section 7.10.2 writes it.
|
| 165 |
-
v = torch.ones_like(TARGET_B)
|
| 166 |
-
Kv = K @ v # Row sums of K, since v = (1, 1).
|
| 167 |
-
u = SOURCE_A / Kv
|
| 168 |
-
Ktu = K.T @ u
|
| 169 |
-
v = TARGET_B / Ktu
|
| 170 |
-
first = scaling_form(u, K, v)
|
| 171 |
-
if loud:
|
| 172 |
-
print(f"\nsweep 1 K v = {[round(float(z), 6) for z in Kv]}"
|
| 173 |
-
f" u = {[round(float(z), 6) for z in u]}")
|
| 174 |
-
print(f" K^T u = {[round(float(z), 6) for z in Ktu]}"
|
| 175 |
-
f" v = {[round(float(z), 5) for z in v]}")
|
| 176 |
-
print(f" diag(u) K diag(v) = {[[round(float(z), 4) for z in row] for row in first]}")
|
| 177 |
-
print(f" column sums {[round(float(z), 4) for z in first.sum(0)]} "
|
| 178 |
-
f"row sums {[round(float(z), 4) for z in first.sum(1)]}")
|
| 179 |
-
after_u = project_rows(first, SOURCE_A) # The next update of u, as a row projection.
|
| 180 |
-
print(f" after the next u update: rows {[round(float(z), 3) for z in after_u.sum(1)]} "
|
| 181 |
-
f"columns {[round(float(z), 3) for z in after_u.sum(0)]}")
|
| 182 |
-
|
| 183 |
-
u, v, errors = sinkhorn_sweeps(K, SOURCE_A, TARGET_B, max(args.steps, 1))
|
| 184 |
-
plan = scaling_form(u, K, v)
|
| 185 |
-
f = multipliers_from_scalings(u, epsilon)
|
| 186 |
-
g = multipliers_from_scalings(v, epsilon)
|
| 187 |
-
sums = f[:, None] + g[None, :] # The entropic counterparts of the dual prices.
|
| 188 |
-
if loud:
|
| 189 |
-
print(f"\nmarginal error by sweep {[round(e, 3) for e in errors[:5]]}")
|
| 190 |
-
print(f" successive ratios "
|
| 191 |
-
f"{[round(errors[i + 1] / errors[i], 2) for i in range(min(4, len(errors) - 1))]}")
|
| 192 |
-
print(f" pi^eps = {[[round(float(z), 6) for z in row] for row in plan]}")
|
| 193 |
-
print(f" u = {[round(float(z), 4) for z in u]} v = {[round(float(z), 4) for z in v]}")
|
| 194 |
-
print(f" f = {[round(float(z), 3) for z in f]} g = {[round(float(z), 3) for z in g]}")
|
| 195 |
-
print(f" f_i + g_j = {[round(float(z), 3) for z in sums.flatten()]} "
|
| 196 |
-
f"against C = {[round(float(z), 0) for z in COST.flatten()]}")
|
| 197 |
-
print(f" the route (2,2) carries {float(plan[1, 1]):.3f} and overshoots its cost by "
|
| 198 |
-
f"{float(sums[1, 1] - COST[1, 1]):.3f}")
|
| 199 |
-
|
| 200 |
-
print("\neps <C, pi^eps> pi^eps_21 sweeps to 1e-9")
|
| 201 |
-
for value in (10.0, 2.0, 0.5, 0.1):
|
| 202 |
-
kernel = gibbs_kernel(COST, value)
|
| 203 |
-
su, sv, _ = sinkhorn_sweeps(kernel, SOURCE_A, TARGET_B, 400)
|
| 204 |
-
p = scaling_form(su, kernel, sv)
|
| 205 |
-
print(f"{value:<7g} {float((COST * p).sum()):<14.7f} {float(p[1, 0]):<16.3e} "
|
| 206 |
-
f"{sweeps_to_tolerance(kernel, SOURCE_A, TARGET_B)}")
|
| 207 |
-
|
| 208 |
-
report = {
|
| 209 |
-
"H(pi*)": entropy(EXACT_PLAN),
|
| 210 |
-
"H(a x b) = H(a) + H(b)": entropy(independent),
|
| 211 |
-
"KL(pi* || a x b)": relative_entropy(EXACT_PLAN, independent),
|
| 212 |
-
"K_11": float(K[0, 0]),
|
| 213 |
-
"K_12": float(K[0, 1]),
|
| 214 |
-
"sweep 1 marginal error": errors[0],
|
| 215 |
-
"sweep 5 marginal error": errors[min(4, len(errors) - 1)],
|
| 216 |
-
"sweeps to 1e-9": sweeps_to_tolerance(K, SOURCE_A, TARGET_B),
|
| 217 |
-
"pi^eps_11": float(plan[0, 0]),
|
| 218 |
-
"pi^eps_21": float(plan[1, 0]),
|
| 219 |
-
"<C, pi^eps>": float((COST * plan).sum()),
|
| 220 |
-
"<C, pi*>": float((COST * EXACT_PLAN).sum()),
|
| 221 |
-
"H(pi^eps)": entropy(plan),
|
| 222 |
-
"objective at pi^eps": entropic_objective(plan, COST, epsilon),
|
| 223 |
-
"objective at pi*": entropic_objective(EXACT_PLAN, COST, epsilon),
|
| 224 |
-
"objective at a x b": entropic_objective(independent, COST, epsilon),
|
| 225 |
-
"KL(pi^eps || a x b)": relative_entropy(plan, independent),
|
| 226 |
-
"f": [round(float(z), 3) for z in f],
|
| 227 |
-
"g": [round(float(z), 3) for z in g],
|
| 228 |
-
}
|
| 229 |
-
print_report("Entropic transport and Sinkhorn, Chapter 7 Sections 7.9 and 7.10", report)
|
| 230 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
if __name__ == "__main__":
|
| 234 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lecture_7/static_bridge.py
DELETED
|
@@ -1,295 +0,0 @@
|
|
| 1 |
-
"""The static Schrodinger bridge: a path law, the chain rule, and the scaling that solves it.
|
| 2 |
-
|
| 3 |
-
A path law over a finite time grid is an ordinary probability vector over paths, and its relative
|
| 4 |
-
entropy from a reference splits by the chain rule into a term for the two endpoints and a term for
|
| 5 |
-
the interior. Only the first term is constrained, so the bridge keeps the reference bridges exactly
|
| 6 |
-
and all of its freedom goes into the endpoint coupling, which iterative proportional fitting
|
| 7 |
-
computes by alternate rescaling.
|
| 8 |
-
|
| 9 |
-
Chapter 7, Sections 7.12 to 7.15.3 of the notes. Runs on the two-state chain with transition matrix
|
| 10 |
-
A, source law (0.5, 0.5) and requested terminal law (0.2, 0.8). Prints the eight path probabilities,
|
| 11 |
-
the chain rule on two tables, the fitted potentials, the tilted matrices and the bridge path law.
|
| 12 |
-
|
| 13 |
-
python lecture_7/static_bridge.py # 4 rounds of proportional fitting
|
| 14 |
-
python lecture_7/static_bridge.py --steps 1 # one round, the one worked by hand
|
| 15 |
-
"""
|
| 16 |
-
|
| 17 |
-
# %% 01. Imports and configuration
|
| 18 |
-
from __future__ import annotations
|
| 19 |
-
|
| 20 |
-
import itertools
|
| 21 |
-
|
| 22 |
-
import torch
|
| 23 |
-
|
| 24 |
-
from cis6270.runner import common_parser, print_report, save_run, set_seed
|
| 25 |
-
|
| 26 |
-
DTYPE = torch.float64 # The chapter quotes six figures of the potentials and of the path law.
|
| 27 |
-
|
| 28 |
-
A = torch.tensor([[0.7, 0.3], [0.4, 0.6]], dtype=DTYPE) # The reference transition matrix.
|
| 29 |
-
PI_0 = torch.tensor([0.5, 0.5], dtype=DTYPE) # The source law.
|
| 30 |
-
PI_1 = torch.tensor([0.2, 0.8], dtype=DTYPE) # The requested terminal law after two steps.
|
| 31 |
-
STEPS = 2 # N, the number of transitions, so a path is a triple and there are eight of them.
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
# %% 02. The probability of a whole path
|
| 35 |
-
#
|
| 36 |
-
# For a Markov process the probability of a list of states is the starting mass times the product of
|
| 37 |
-
# the transition probabilities along it. Section 7.12.1.
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def path_law(start: torch.Tensor, matrices: list[torch.Tensor]) -> dict[tuple[int, ...], float]:
|
| 41 |
-
"""R(x_{0:N}) over every path of the chain, as a dictionary keyed by the list of states."""
|
| 42 |
-
size = len(start)
|
| 43 |
-
law = {}
|
| 44 |
-
for path in itertools.product(range(size), repeat=len(matrices) + 1):
|
| 45 |
-
weight = float(start[path[0]]) # The starting mass R_0(x_0).
|
| 46 |
-
for k, matrix in enumerate(matrices):
|
| 47 |
-
weight *= float(matrix[path[k], path[k + 1]]) # One transition probability per step.
|
| 48 |
-
law[path] = weight
|
| 49 |
-
return law # K^(N+1) entries, summing to one.
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def relative_entropy(p: dict, q: dict) -> float:
|
| 53 |
-
"""KL(P || R) = sum over paths of P log(P / R), the definition of Chapter 1 indexed by paths."""
|
| 54 |
-
return sum(v * torch.log(torch.tensor(v / q[k], dtype=DTYPE)).item()
|
| 55 |
-
for k, v in p.items() if v > 0)
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
# %% 03. The chain rule for relative entropy
|
| 59 |
-
#
|
| 60 |
-
# Factoring each joint law into a marginal and a conditional and averaging the logarithm of the
|
| 61 |
-
# ratio splits the relative entropy into a marginal term and an averaged conditional term.
|
| 62 |
-
# Sections 7.13 and 7.13.1.
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def kl_vectors(p: torch.Tensor, q: torch.Tensor) -> float:
|
| 66 |
-
"""Relative entropy between two probability vectors over the same states."""
|
| 67 |
-
mask = p > 0
|
| 68 |
-
return float((p[mask] * (p[mask] / q[mask]).log()).sum())
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def chain_rule_terms(pi: torch.Tensor, other: torch.Tensor) -> tuple[float, float, float]:
|
| 72 |
-
"""The total, the marginal term and the averaged conditional term of KL(pi || other)."""
|
| 73 |
-
total = kl_vectors(pi.flatten(), other.flatten()) # The four cells, computed directly.
|
| 74 |
-
marginal = kl_vectors(pi.sum(dim=1), other.sum(dim=1)) # The disagreement about the row.
|
| 75 |
-
rows = [kl_vectors(pi[i] / pi[i].sum(), other[i] / other[i].sum()) for i in range(len(pi))]
|
| 76 |
-
conditional = float((pi.sum(dim=1) * torch.tensor(rows, dtype=DTYPE)).sum())
|
| 77 |
-
return total, marginal, conditional
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
# %% 04. Splitting a path law at its two endpoints
|
| 81 |
-
#
|
| 82 |
-
# With the pair (x_0, x_N) in the place of the first coordinate and the interior in the place of the
|
| 83 |
-
# second, the chain rule separates the endpoint coupling from the bridges. The second term is free
|
| 84 |
-
# and nonnegative, so the bridge takes the reference bridges and the problem collapses onto the
|
| 85 |
-
# coupling. Section 7.13.2.
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
def endpoint_coupling(law: dict) -> torch.Tensor:
|
| 89 |
-
"""The law of the pair (x_0, x_N) under a path law, summed over the interior states."""
|
| 90 |
-
coupling = torch.zeros(2, 2, dtype=DTYPE)
|
| 91 |
-
for path, weight in law.items():
|
| 92 |
-
coupling[path[0], path[-1]] += weight # Collect every path with the same two ends.
|
| 93 |
-
return coupling
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def reference_bridge(x0: int, mid: int, xN: int, matrix: torch.Tensor,
|
| 97 |
-
two_step: torch.Tensor) -> float:
|
| 98 |
-
"""R(x_1 | x_0, x_2) = A(x_0, x_1) A(x_1, x_2) / A^2(x_0, x_2), the interior given the ends."""
|
| 99 |
-
return float(matrix[x0, mid] * matrix[mid, xN] / two_step[x0, xN])
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def fill_with_reference_bridges(coupling: torch.Tensor, matrix: torch.Tensor) -> dict:
|
| 103 |
-
"""P(x_{0:2}) = pi(x_0, x_2) R(x_1 | x_0, x_2), a coupling filled in with reference bridges."""
|
| 104 |
-
two_step = matrix @ matrix
|
| 105 |
-
return {(x0, mid, xN): float(coupling[x0, xN]) * reference_bridge(x0, mid, xN, matrix, two_step)
|
| 106 |
-
for x0, mid, xN in itertools.product(range(2), repeat=3)}
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
# %% 05. The static problem and the two potentials
|
| 110 |
-
#
|
| 111 |
-
# The bridge coupling minimizes KL(pi || R_01) over the couplings of pi_0 and pi_1, which is the
|
| 112 |
-
# entropic problem with R_01 in place of the Gibbs kernel, so the minimizer is R_01 with one scaling
|
| 113 |
-
# per row and one per column and the two marginals determine them by alternation.
|
| 114 |
-
# Sections 7.14 and 7.14.2.
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
def reference_endpoint_law(start: torch.Tensor, matrix: torch.Tensor, steps: int) -> torch.Tensor:
|
| 118 |
-
"""R_01 = diag(pi_0) A^N, the reference probability of starting at x and ending at y."""
|
| 119 |
-
return torch.diag(start) @ torch.linalg.matrix_power(matrix, steps) # [K, K].
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
def proportional_fitting(
|
| 123 |
-
R01: torch.Tensor, start: torch.Tensor, end: torch.Tensor, rounds: int,
|
| 124 |
-
) -> tuple[torch.Tensor, torch.Tensor, list[float]]:
|
| 125 |
-
"""Alternate g <- pi_1 / (R_01^T f) and f <- pi_0 / (R_01 g), returning the terminal errors.
|
| 126 |
-
|
| 127 |
-
One round is a g update followed by an f update, started from f = (1, 1), so the row sums are
|
| 128 |
-
exact at the end of a round and the error is measured on the columns.
|
| 129 |
-
"""
|
| 130 |
-
f = torch.ones_like(start)
|
| 131 |
-
g = torch.ones_like(end)
|
| 132 |
-
errors: list[float] = []
|
| 133 |
-
for _ in range(rounds):
|
| 134 |
-
g = end / (R01.T @ f) # Column sums become exactly pi_1.
|
| 135 |
-
f = start / (R01 @ g) # Row sums become exactly pi_0, and the columns move again.
|
| 136 |
-
plan = f[:, None] * R01 * g[None, :]
|
| 137 |
-
errors.append(float((plan.sum(dim=0) - end).abs().sum()))
|
| 138 |
-
return f, g, errors
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
# %% 06. The squared cost from a Brownian reference
|
| 142 |
-
#
|
| 143 |
-
# For a Brownian motion of variance epsilon the transition density is a Gibbs kernel for the squared
|
| 144 |
-
# distance at strength 2 epsilon, so minimizing the relative entropy from its endpoint law is the
|
| 145 |
-
# entropic transport problem with the noise variance in the place of the regularization strength.
|
| 146 |
-
# Section 7.14.1.
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
def brownian_reference_plan(epsilon: float = 1.0) -> torch.Tensor:
|
| 150 |
-
"""The bridge coupling of the four points of Section 7.3.1 under a Brownian reference."""
|
| 151 |
-
x = torch.tensor([0.0, 2.0], dtype=DTYPE) # Source points.
|
| 152 |
-
y = torch.tensor([1.0, 3.0], dtype=DTYPE) # Target points.
|
| 153 |
-
a = torch.tensor([0.6, 0.4], dtype=DTYPE) # Their masses.
|
| 154 |
-
b = torch.tensor([0.3, 0.7], dtype=DTYPE)
|
| 155 |
-
endpoint_law = torch.exp(-(x[:, None] - y[None, :]) ** 2 / (2.0 * epsilon)) # exp(-C/2).
|
| 156 |
-
f, g, _ = proportional_fitting(endpoint_law, a, b, 200)
|
| 157 |
-
return f[:, None] * endpoint_law * g[None, :] # [2, 2].
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
# %% 07. Making the bridge a Markov process
|
| 161 |
-
#
|
| 162 |
-
# The backward potential averages g over the reference, one matrix-vector product per step, and
|
| 163 |
-
# reweighting each transition by the ratio of the potential at the destination to the potential at
|
| 164 |
-
# the current state produces a transition matrix whose rows sum to one by that same recursion.
|
| 165 |
-
# Sections 7.15 and 7.15.1.
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
def backward_potential(matrix: torch.Tensor, g: torch.Tensor, steps: int) -> list[torch.Tensor]:
|
| 169 |
-
"""h_N = g and h_k = A h_{k+1}, returned as the list h_0, ..., h_N."""
|
| 170 |
-
potentials = [g]
|
| 171 |
-
for _ in range(steps):
|
| 172 |
-
potentials.insert(0, matrix @ potentials[0]) # One step back, into the front of the list.
|
| 173 |
-
return potentials
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
def tilted_matrix(matrix: torch.Tensor, h_now: torch.Tensor, h_next: torch.Tensor) -> torch.Tensor:
|
| 177 |
-
"""A^h_k(x, y) = A(x, y) h_{k+1}(y) / h_k(x), Doob's h-transform of one step."""
|
| 178 |
-
return matrix * h_next[None, :] / h_now[:, None] # [K, K], each row summing to one.
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
# %% 08. The report
|
| 182 |
-
def main() -> None:
|
| 183 |
-
parser = common_parser(__doc__.split("\n")[0])
|
| 184 |
-
parser.set_defaults(steps=4) # --steps is the number of proportional fitting rounds.
|
| 185 |
-
args = parser.parse_args()
|
| 186 |
-
set_seed(args.seed)
|
| 187 |
-
loud = not args.quiet
|
| 188 |
-
rounds = max(args.steps, 1)
|
| 189 |
-
|
| 190 |
-
reference = path_law(PI_0, [A] * STEPS)
|
| 191 |
-
if loud:
|
| 192 |
-
print("The eight paths of the two-state chain, with their reference probabilities")
|
| 193 |
-
for path, weight in reference.items():
|
| 194 |
-
print(f" {','.join(str(s + 1) for s in path)} R = {weight:.6f}")
|
| 195 |
-
print(f" they sum to {sum(reference.values()):.6f}")
|
| 196 |
-
|
| 197 |
-
table = torch.tensor([[0.5, 0.1], [0.1, 0.3]], dtype=DTYPE) # pi of Section 7.13.1.
|
| 198 |
-
uniform = torch.full((2, 2), 0.25, dtype=DTYPE) # pi', the uniform law on the four cells.
|
| 199 |
-
total, marginal, conditional = chain_rule_terms(table, uniform)
|
| 200 |
-
if loud:
|
| 201 |
-
print(f"\nChain rule on two tables: total {total:.4f} = marginal {marginal:.4f} "
|
| 202 |
-
f"+ conditional {conditional:.4f}")
|
| 203 |
-
|
| 204 |
-
R01 = reference_endpoint_law(PI_0, A, STEPS)
|
| 205 |
-
reference_error = float((R01.sum(dim=0) - PI_1).abs().sum())
|
| 206 |
-
f, g, errors = proportional_fitting(R01, PI_0, PI_1, rounds)
|
| 207 |
-
plan = f[:, None] * R01 * g[None, :]
|
| 208 |
-
independent = PI_0[:, None] * PI_1[None, :]
|
| 209 |
-
if loud:
|
| 210 |
-
print(f"\nA^2 = {[[round(float(z), 2) for z in row] for row in A @ A]}")
|
| 211 |
-
print(f"R_01 = {[[round(float(z), 3) for z in row] for row in R01]} "
|
| 212 |
-
f"column sums {[round(float(z), 3) for z in R01.sum(0)]} error {reference_error:.2f}")
|
| 213 |
-
first_g = PI_1 / R01.sum(dim=0) # The first update, with f = (1, 1).
|
| 214 |
-
print(f" first g = {[round(float(z), 4) for z in first_g]} "
|
| 215 |
-
f"R_01 g = {[round(float(z), 4) for z in R01 @ first_g]}")
|
| 216 |
-
print(f" first f = {[round(float(z), 4) for z in PI_0 / (R01 @ first_g)]}")
|
| 217 |
-
print(" round f g terminal error ratio")
|
| 218 |
-
fr = torch.ones_like(PI_0)
|
| 219 |
-
for index in range(rounds):
|
| 220 |
-
gr = PI_1 / (R01.T @ fr)
|
| 221 |
-
fr = PI_0 / (R01 @ gr)
|
| 222 |
-
previous = reference_error if index == 0 else errors[index - 1]
|
| 223 |
-
ratio = f"{previous / errors[index]:.1f}" if errors[index] > 0 else "exact"
|
| 224 |
-
print(f" {index + 1:<7d} ({float(fr[0]):.6f}, {float(fr[1]):.6f}) "
|
| 225 |
-
f"({float(gr[0]):.6f}, {float(gr[1]):.6f}) {errors[index]:.3e} {ratio}")
|
| 226 |
-
print(f" pi* = {[[round(float(z), 6) for z in row] for row in plan]}")
|
| 227 |
-
print(f" KL(pi* || R_01) = {kl_vectors(plan.flatten(), R01.flatten()):.4f} "
|
| 228 |
-
f"KL(independent || R_01) = {kl_vectors(independent.flatten(), R01.flatten()):.4f}")
|
| 229 |
-
|
| 230 |
-
brownian = brownian_reference_plan()
|
| 231 |
-
if loud:
|
| 232 |
-
print("\nBrownian reference at epsilon = 1 on the four points of Section 7.3.1")
|
| 233 |
-
print(f" pi* = {[[round(float(z), 4) for z in row] for row in brownian]}, the plan the "
|
| 234 |
-
f"Gibbs kernel at strength 2 produced")
|
| 235 |
-
|
| 236 |
-
potentials = backward_potential(A, g, STEPS)
|
| 237 |
-
tilted = [tilted_matrix(A, potentials[k], potentials[k + 1]) for k in range(STEPS)]
|
| 238 |
-
middle = PI_0 @ tilted[0]
|
| 239 |
-
terminal = middle @ tilted[1]
|
| 240 |
-
bridge_law = fill_with_reference_bridges(plan, A)
|
| 241 |
-
bridge_kl = relative_entropy(bridge_law, reference)
|
| 242 |
-
middle_from_paths = sum(v for k, v in bridge_law.items() if k[1] == 0)
|
| 243 |
-
if loud:
|
| 244 |
-
print()
|
| 245 |
-
for k, h in enumerate(potentials):
|
| 246 |
-
print(f"h_{k} = ({float(h[0]):.6f}, {float(h[1]):.6f}) "
|
| 247 |
-
f"ratio {float(h[1] / h[0]):.2f}")
|
| 248 |
-
for k, matrix in enumerate(tilted):
|
| 249 |
-
print(f"A^h_{k} = {[[round(float(z), 6) for z in row] for row in matrix]}")
|
| 250 |
-
print(f" (0.5, 0.5) -> {[round(float(z), 6) for z in middle]} -> "
|
| 251 |
-
f"{[round(float(z), 6) for z in terminal]}")
|
| 252 |
-
print(f" 1 / h_0 = {[round(float(z), 6) for z in 1.0 / potentials[0]]}, "
|
| 253 |
-
f"the f the scaling produced")
|
| 254 |
-
product = torch.diag(PI_0) @ tilted[0] @ tilted[1] # The endpoint law of the tilted chain.
|
| 255 |
-
print(f" diag(pi_0) A^h_0 A^h_1 = "
|
| 256 |
-
f"{[[round(float(z), 6) for z in row] for row in product]}")
|
| 257 |
-
print("\npath R P* P*/R")
|
| 258 |
-
for path in reference:
|
| 259 |
-
name = ",".join(str(s + 1) for s in path)
|
| 260 |
-
print(f" {name} {reference[path]:.6f} {bridge_law[path]:.6f} "
|
| 261 |
-
f"{bridge_law[path] / reference[path]:.6f}")
|
| 262 |
-
|
| 263 |
-
report = {
|
| 264 |
-
"R(1,1,2)": reference[(0, 0, 1)],
|
| 265 |
-
"the eight paths sum to": sum(reference.values()),
|
| 266 |
-
"chain rule, total": total,
|
| 267 |
-
"chain rule, marginal term": marginal,
|
| 268 |
-
"chain rule, conditional term": conditional,
|
| 269 |
-
"reference terminal error": reference_error,
|
| 270 |
-
"terminal error, round 1": errors[0],
|
| 271 |
-
"terminal error, last round": errors[-1],
|
| 272 |
-
"error ratio per round": reference_error / errors[0],
|
| 273 |
-
"f": [round(float(z), 6) for z in f],
|
| 274 |
-
"g": [round(float(z), 6) for z in g],
|
| 275 |
-
"pi*_11": float(plan[0, 0]),
|
| 276 |
-
"pi*_12": float(plan[0, 1]),
|
| 277 |
-
"KL(pi* || R_01)": kl_vectors(plan.flatten(), R01.flatten()),
|
| 278 |
-
"KL(pi_0 x pi_1 || R_01)": kl_vectors(independent.flatten(), R01.flatten()),
|
| 279 |
-
"h_0": [round(float(z), 6) for z in potentials[0]],
|
| 280 |
-
"h_2 = g": [round(float(z), 6) for z in potentials[-1]],
|
| 281 |
-
"A^h_0(1,1)": float(tilted[0][0, 0]),
|
| 282 |
-
"A^h_1(1,2)": float(tilted[1][0, 1]),
|
| 283 |
-
"rows of A^h_0 sum to": float(tilted[0].sum(dim=1).max()),
|
| 284 |
-
"marginal at the middle step": float(middle[0]),
|
| 285 |
-
"marginal at the last step": [round(float(z), 6) for z in terminal],
|
| 286 |
-
"f against 1 / h_0": float((f - 1.0 / potentials[0]).abs().max()),
|
| 287 |
-
"KL(P* || R) over the paths": bridge_kl,
|
| 288 |
-
"middle state from the paths": middle_from_paths,
|
| 289 |
-
}
|
| 290 |
-
print_report("The static Schrodinger bridge, Chapter 7 Sections 7.12 to 7.15.3", report)
|
| 291 |
-
save_run(args.out, config=vars(args), report=report) # Written only with --out.
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
if __name__ == "__main__":
|
| 295 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|