File size: 10,545 Bytes
0ba9d09 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | """Diffusion Schrodinger Bridge: iterative proportional fitting run on path laws.
Each half-step of proportional fitting is a relative-entropy projection onto the set of path laws
with one marginal correct, and the chain rule solves it in closed form by keeping the conditional
law of the previous iterate and resetting that one marginal. The conditional law is the current
process run backward in time, so each half-step reverses the current process and restarts it from
the requested law at the other end.
Chapter 7, Section 7.19.1 of the notes. Runs the half-steps exactly on the two-state chain with
transition matrix A, source law (0.5, 0.5) and requested terminal law (0.2, 0.8), and checks the
diffusion time reversal in closed form on a Gaussian. Prints the backward matrices, the law after
each half-step, the forward matrices of the first round and the error at the free end.
python lecture_7/dsb.py # 4 rounds, the first one worked by hand
python lecture_7/dsb.py --steps 1 # the single round the chapter writes out
"""
# %% 01. Imports and configuration
from __future__ import annotations
import math
import torch
from cis6270.runner import common_parser, print_report, save_run, set_seed
DTYPE = torch.float64 # The chapter quotes six figures of the backward and forward matrices.
A = torch.tensor([[0.7, 0.3], [0.4, 0.6]], dtype=DTYPE) # The reference transition matrix.
PI_0 = torch.tensor([0.5, 0.5], dtype=DTYPE) # The source law.
PI_1 = torch.tensor([0.2, 0.8], dtype=DTYPE) # The requested terminal law.
TILTED_0 = torch.tensor([[0.599160, 0.400840], [0.299266, 0.700734]], dtype=DTYPE) # A^h_0.
TILTED_1 = torch.tensor([[0.307297, 0.692703], [0.112490, 0.887510]], dtype=DTYPE) # A^h_1.
# %% 02. A path law as a joint table
#
# With two states and two steps a path law is a table over the triple (x_0, x_1, x_2), so every
# projection below is an exact operation on eight numbers. Section 7.12.1.
def reference_path_law(start: torch.Tensor, matrix: torch.Tensor, steps: int) -> torch.Tensor:
"""R(x_{0:N}) as a tensor with one axis per time, from the starting law and one matrix per step."""
law = start.clone() # [K] the law at time 0.
for _ in range(steps):
law = law.unsqueeze(-1) * matrix # Multiply by the transition matrix on a new axis.
return law # [K] * (N + 1).
def marginal(law: torch.Tensor, axis: int) -> torch.Tensor:
"""The law of the state at one time, summed over every other time."""
other = [d for d in range(law.dim()) if d != axis]
return law.sum(dim=other) # [K].
def one_step_conditional(law: torch.Tensor, step: int) -> torch.Tensor:
"""P(x_{k+1} | x_k), the forward transition matrix the path law carries at that step."""
pair = law.sum(dim=[d for d in range(law.dim()) if d not in (step, step + 1)]) # [K, K].
return pair / pair.sum(dim=1, keepdim=True) # Normalize each row.
def reverse_step(law: torch.Tensor, step: int) -> torch.Tensor:
"""P(x_k | x_{k+1}), Bayes' rule on one step, the reversal a network has to learn."""
pair = law.sum(dim=[d for d in range(law.dim()) if d not in (step, step + 1)]) # [K, K].
return (pair / pair.sum(dim=0, keepdim=True)).T # [K, K] indexed by the later state first.
# %% 03. The two half-steps
#
# The odd iterate keeps the conditional law of the previous iterate given the final state and
# replaces the marginal at time 1, and the even iterate does the same at time 0. Section 7.19.1.
def fix_terminal(law: torch.Tensor, end: torch.Tensor) -> torch.Tensor:
"""P'(x_{0:N}) = pi_1(x_N) P(x_{0:N-1} | x_N), the projection onto the laws ending at pi_1."""
last = marginal(law, law.dim() - 1) # [K] the terminal marginal of the current law.
return law * (end / last) # Reweight every path by the ratio at its final state.
def fix_initial(law: torch.Tensor, start: torch.Tensor) -> torch.Tensor:
"""P''(x_{0:N}) = pi_0(x_0) P'(x_{1:N} | x_0), the projection onto the laws starting at pi_0."""
first = marginal(law, 0) # [K] the initial marginal of the current law.
shape = [-1] + [1] * (law.dim() - 1) # Broadcast the ratio along the first axis.
return law * (start / first).reshape(shape)
def proportional_fitting(law: torch.Tensor, start: torch.Tensor, end: torch.Tensor,
rounds: int) -> tuple[torch.Tensor, list[float], list[torch.Tensor]]:
"""Alternate the two half-steps, returning the law, the terminal error and the law each round."""
errors: list[float] = []
iterates: list[torch.Tensor] = []
for _ in range(rounds):
law = fix_terminal(law, end) # One half-step, and the terminal marginal is then exact.
law = fix_initial(law, start) # The other, and the initial marginal is then exact.
errors.append(float((marginal(law, law.dim() - 1) - end).abs().sum()))
iterates.append(law.clone())
return law, errors, iterates
# %% 04. The reversal that a network learns
#
# On a finite state space the reversal is Bayes' rule on a matrix. On R^d it is the time reversal of
# a diffusion, with drift minus the forward drift plus epsilon times the score of the current
# marginal. The score of the current iterate has no closed form. Section 7.19.1.
def reverse_drift(x: torch.Tensor, t: torch.Tensor, epsilon: float, variance_0: float
) -> torch.Tensor:
"""beta(x, t) = -b + epsilon grad log p_t for a driftless diffusion started from N(0, v_0)."""
variance = variance_0 + epsilon * t # The marginal of sqrt(epsilon) dB started from N(0, v_0).
return epsilon * (-x / variance) # The score of a centered Gaussian is -x over its variance.
def reversal_check(epsilon: float, variance_0: float, steps: int, paths: int,
generator: torch.Generator) -> tuple[float, float]:
"""Run the reversal backward from p_1 and compare its variance at time 0 with v_0."""
dt = 1.0 / steps
variance_1 = variance_0 + epsilon * 1.0 # The forward marginal at time 1.
x = torch.randn(paths, dtype=DTYPE, generator=generator) * math.sqrt(variance_1)
for step in range(steps):
t = torch.tensor(1.0 - step * dt, dtype=DTYPE) # Time runs down from 1 to 0.
noise = torch.randn(paths, dtype=DTYPE, generator=generator) * math.sqrt(dt)
x = x + reverse_drift(x, t, epsilon, variance_0) * dt + math.sqrt(epsilon) * noise
return float(x.var()), variance_0
# %% 05. The report
def main() -> None:
parser = common_parser(__doc__.split("\n")[0])
parser.set_defaults(steps=4) # --steps is the number of proportional fitting rounds.
args = parser.parse_args()
set_seed(args.seed)
loud = not args.quiet
rounds = max(args.steps, 1)
generator = torch.Generator().manual_seed(args.seed)
reference = reference_path_law(PI_0, A, 2) # [2, 2, 2] the law P^0 = R.
marginals = [marginal(reference, k) for k in range(3)]
B2 = reverse_step(reference, 1) # P(x_1 | x_2), the reversal of the last step.
B1 = reverse_step(reference, 0) # P(x_0 | x_1), the reversal of the earlier step.
if loud:
print("The reference started from pi_0, read at the three time points")
print(f" {[[round(float(z), 3) for z in m] for m in marginals]}")
print(f" B_2 = {[[round(float(z), 6) for z in row] for row in B2]}")
print(f" B_1(1,1) = {float(B1[0, 0]):.6f}")
half = fix_terminal(reference, PI_1) # The first half-step, a reset of the terminal marginal.
backward_laws = [marginal(half, 2), marginal(half, 1), marginal(half, 0)]
drift_at_zero = float((backward_laws[2] - PI_0).abs().sum())
full = fix_initial(half, PI_0) # The second half-step, a reset of the initial marginal.
F0 = one_step_conditional(full, 0)
F1 = one_step_conditional(full, 1)
terminal = marginal(full, 2)
terminal_error = float((terminal - PI_1).abs().sum())
tilt_gap = max(float((F0 - TILTED_0).abs().max()), float((F1 - TILTED_1).abs().max()))
if loud:
print("\nThe backward chain started from the requested terminal law")
written = ["(" + ", ".join(f"{float(z):.6f}" for z in m) + ")" for m in backward_laws]
print(f" {' -> '.join(written)}")
print(f" total absolute error at step 0: {drift_at_zero:.4f}")
print(f"\nThe forward matrices of the second half-step")
print(f" F_0 = {[[round(float(z), 6) for z in row] for row in F0]}")
print(f" F_1 = {[[round(float(z), 6) for z in row] for row in F1]}")
print(f" terminal law {[round(float(z), 6) for z in terminal]}, "
f"off by {terminal_error:.4f}")
print(f" largest gap against the tilted matrices A^h_0 and A^h_1: {tilt_gap:.4f}")
_, errors, _ = proportional_fitting(reference, PI_0, PI_1, rounds)
ratios = [errors[k] / errors[k + 1] for k in range(len(errors) - 1) if errors[k + 1] > 0]
if loud:
print(f"\nterminal error by round {['%.3e' % e for e in errors]}")
print(f" ratios {[round(r, 1) for r in ratios]}")
simulated, exact = reversal_check(1.0, 1.0, 200, 8192, generator)
if loud:
print(f"\nThe diffusion reversal on a Gaussian, run backward from p_1")
print(f" variance at time 0: simulated {simulated:.3f}, exact {exact:.3f}")
report = {
"reference marginal at step 1": [round(float(z), 3) for z in marginals[1]],
"reference marginal at step 2": [round(float(z), 3) for z in marginals[2]],
"B_2(1,1)": float(B2[0, 0]),
"B_2(2,1)": float(B2[1, 0]),
"B_1(1,1)": float(B1[0, 0]),
"backward law after one step": [round(float(z), 6) for z in backward_laws[1]],
"backward law at step 0": [round(float(z), 6) for z in backward_laws[2]],
"error at the free end, half-step": drift_at_zero,
"F_0(1,1)": float(F0[0, 0]),
"F_1(1,2)": float(F1[0, 1]),
"terminal law after one round": [round(float(z), 6) for z in terminal],
"terminal error after one round": terminal_error,
"gap against the tilted matrices": tilt_gap,
"terminal error, last round": errors[-1],
"error ratio per round": ratios[0] if ratios else float("nan"),
"rounds run": rounds,
"reversal variance, simulated": simulated,
"reversal variance, exact": exact,
}
print_report("Diffusion Schrodinger Bridge, Chapter 7 Section 7.19.1", report)
save_run(args.out, config=vars(args), report=report) # Written only with --out.
if __name__ == "__main__":
main()
|