File size: 12,675 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 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | """Diamond Maps and Meta Flow Maps, the posterior over endpoints that guidance needs.
To steer a flow toward a reward we need the value function, the log expected exponential reward
over the terminal states reachable from the current one, and that average runs over a posterior. A
deterministic map returns one endpoint, so it can only estimate the value by the reward at that
endpoint, low by the Jensen gap. If we give the map a fresh noise argument and index it by the
state it is conditioned on, it becomes a family of inner flow maps, one per conditioning pair,
and their draws are posterior samples.
Chapter 6, Sections 6.33 and 6.34 of the notes. Runs on a two-point reward problem and on the four
Gaussian blobs of ``cis6270.data.gaussian_mixture_2d``. Prints the Jensen gap of 0.434, the bias
of the K-sample value at K = 1, 2, 4 and 8, the softmax-weighted value gradient of -0.4483, the
importance weight that pulls a renoised proposal from 0.420 back to 0.0131, and the stop-gradient
fixed point 0.625 of Meta Flow Maps.
python lecture_6/posterior_map.py # 16 inner samples per state
python lecture_6/posterior_map.py --steps 20 # fewer inner substeps, for a first look
"""
# %% 01. Imports and configuration
import math
import torch
from cis6270.data import gaussian_mixture_2d
from cis6270.runner import common_parser, pick_device, print_report, save_run, set_seed
torch.set_num_threads(1) # The work here is small, and one thread keeps the timing stable.
# ``tests/conftest.py`` sets the same thing for the test session, and this line acts when a reader
# runs the script directly.
torch.set_default_dtype(torch.float64) # Exact posteriors, compared against sample averages.
INNER_STEPS = 48 # Substeps of the inner flow, the leg from noise to a posterior sample.
# %% 02. The tilt, the value function and the Jensen gap
#
# The tilted target is the data times the exponential reward, and the same reweighting acts on the
# posterior over endpoints at every intermediate state. A deterministic map returns the posterior
# mean, so its estimate of the value is the reward there, and the exponential is convex, so that
# estimate is always the low one.
def two_point(reward_gap: float = 2.0, x: float = 0.7, t: float = 0.8):
"""The chapter's two-point example, with z in {0, 2}, r(z) = z and a linear schedule."""
weights = torch.tensor([1.0, math.exp(reward_gap)]) # e^{r(0)} and e^{r(2)}.
tilted = weights / weights.sum() # The tilted target over the two points.
far = math.exp(-((x - t * 2.0) ** 2) / (2.0 * (1.0 - t) ** 2)) # Unnormalized, at z = 2.
near = math.exp(-(x ** 2) / (2.0 * (1.0 - t) ** 2)) # Unnormalized, at z = 0.
posterior = torch.tensor([near, far]) / (near + far) # The posterior over endpoints at x.
value = math.log(float((posterior * weights).sum())) # V_t(x) at that state.
tilted_posterior = posterior * weights / float((posterior * weights).sum())
exact = math.log(0.5 + 0.5 * math.exp(reward_gap)) # V over the data itself.
return {
"tilted target": [round(v, 4) for v in tilted.tolist()],
"posterior at x=0.7": [round(v, 4) for v in posterior.tolist()],
"tilted posterior": [round(v, 4) for v in tilted_posterior.tolist()],
"V_t(0.7)": value,
"V over the data": exact,
"single-endpoint estimate": 1.0 * reward_gap / 2.0, # r at the posterior mean z = 1.
"Jensen gap": exact - reward_gap / 2.0,
}
def value_bias(reward_gap: float = 2.0):
"""E[V_K] over K independent posterior draws, and it approaches the exact value like 1/K."""
exact = math.log(0.5 + 0.5 * math.exp(reward_gap))
out = {}
for k in (1, 2, 4, 8):
expectation = sum(
math.comb(k, j) * 2.0 ** (-k)
* math.log(((k - j) + j * math.exp(reward_gap)) / k) for j in range(k + 1))
out[f"E[V_{k}]"] = expectation
out[f"bias at K={k}"] = exact - expectation
return out
# %% 03. The value gradient from K samples
#
# If we differentiate the logarithm of an average of exponentials we get a softmax over the
# rewards, so a sample whose reward is far above the others dominates the direction and a set with
# equal rewards gives their plain average.
def value_gradient(rewards, gradients):
"""The softmax weights over the rewards, and the weighted average of the reward gradients."""
weights = torch.softmax(rewards, dim=0) # [K].
return weights, float((weights * gradients).sum()), float(gradients.mean())
# %% 04. The inner clock
#
# For a fixed conditioning pair the map is an ordinary two-time flow map on an inner time axis
# running from the noise draw at 0 to a posterior sample at 1. Its velocity is the flow matching
# velocity of the posterior, available in closed form over the training points.
def posterior_weights(x, t, data):
"""p_{1|t}(z_i | x), a softmax over the training points under the linear interpolant. [B, N]."""
spread = max(1.0 - t, 1e-6)
squared = ((x[:, None, :] - t * data[None]) ** 2).sum(-1) # [B, N].
return torch.softmax(-squared / (2.0 * spread ** 2), dim=1) # [B, N].
def inner_map(noise, weights, data, substeps: int):
"""Y_{0,1}(eps | x, t), the inner flow from a Gaussian draw to a posterior sample. [B, K, 2]."""
y = noise # [B, K, 2] one inner state per sample.
step = 1.0 / substeps
for i in range(substeps):
a = i * step # The inner time.
squared = ((y[:, :, None, :] - a * data[None, None]) ** 2).sum(-1) # [B, K, N].
inner = torch.softmax(torch.log(weights[:, None, :].clamp_min(1e-30))
- squared / (2.0 * max(1.0 - a, 1e-6) ** 2), dim=-1) # [B, K, N].
denoiser = inner @ data # [B, K, 2] the inner posterior mean.
y = y + step * (denoiser - y) / max(1.0 - a, 1e-6) # Euler on the inner velocity.
return y
# %% 05. Posterior samples from a map we already have
#
# We can renoise the current state back to an earlier time with fresh noise and push it through
# the existing map. That discards information and widens the law of the endpoint. The correction
# is an importance weight, written as a shift of the reward by the log likelihood of the current
# state given that endpoint.
def renoise_example(x: float = 0.7, t: float = 0.8, earlier: float = 0.6):
"""The chapter's renoised proposal, and the importance weight that recovers the posterior."""
scale = earlier / t # alpha_{t'}/alpha_t on the linear schedule.
variance = (1.0 - earlier) ** 2 - scale ** 2 * (1.0 - t) ** 2 # The variance added back.
mean = scale * x # The center of the proposal.
boundary = (earlier * 2.0 / 2.0 - mean) / math.sqrt(variance) # Where the map switches mode.
proposal_far = 0.5 * math.erfc(boundary / math.sqrt(2.0)) # Share of draws reaching z = 2.
penalty_far = (x - t * 2.0) ** 2 / (2.0 * (1.0 - t) ** 2) # Log likelihood of x given z = 2.
penalty_near = x ** 2 / (2.0 * (1.0 - t) ** 2) # The same for z = 0.
far = proposal_far * math.exp(-penalty_far)
near = (1.0 - proposal_far) * math.exp(-penalty_near)
return {"renoise scale": scale, "renoise variance": variance,
"renoise standard deviation": math.sqrt(variance),
"proposal weight on the far mode": proposal_far,
"penalty at z=2": penalty_far, "penalty at z=0": penalty_near,
"corrected weight on the far mode": far / (far + near)}
# %% 06. Meta Flow Maps, the stop-gradient form
#
# The controlled velocity needs a ratio of two expectations, and if we regress onto a per-sample
# ratio the result is biased at every sample size. If we multiply through by the denominator, the
# condition that remains carries one stop-gradient, and its fixed point is the ratio of the
# expectations.
def stop_gradient_fixed_point(weights, gradients, diffusion_squared: float = 2.0):
"""The fixed point of the loss, the per-sample ratio it is compared against, and the gap."""
exact = 0.5 * diffusion_squared * float(gradients.mean()) / float(weights.mean())
naive = float((gradients / weights).mean()) * 0.5 * diffusion_squared
covariance = float(gradients.mean()) - float((gradients / weights).mean() * weights.mean())
return {"E[w]": float(weights.mean()), "E[G]": float(gradients.mean()),
"stop-gradient fixed point": exact, "per-sample ratio average": naive,
"gap": naive - exact, "covariance of G/w and w": covariance,
"gap from the covariance": -covariance / float(weights.mean())}
# %% 07. The report
def main() -> None:
parser = common_parser(__doc__.split("\n")[0])
parser.set_defaults(steps=INNER_STEPS, samples=16)
args = parser.parse_args() # --steps is the inner substep count here, since nothing trains.
set_seed(args.seed)
pick_device(args.device) # Every number here is double precision, and not every
# accelerator carries it, so the work stays on the CPU whatever --device names.
substeps = max(args.steps, 8)
report = two_point()
report.update(value_bias())
weights, weighted, plain = value_gradient(torch.tensor([0.2, 1.5, 0.9]),
torch.tensor([0.4, -1.2, 0.5]))
report["softmax weights"] = [round(v, 4) for v in weights.tolist()]
report["weighted value gradient"] = weighted
report["plain average gradient"] = plain
report.update(renoise_example())
report.update(stop_gradient_fixed_point(torch.tensor([1.0, 2.0, 5.0]),
torch.tensor([0.4, 1.0, 3.6])))
if not args.quiet:
print(f"Jensen gap {report['Jensen gap']:.3f} between the exact value "
f"{report['V over the data']:.4f} and the single endpoint "
f"{report['single-endpoint estimate']:.3f}")
print(f"renoised proposal {report['proposal weight on the far mode']:.3f} corrected to "
f"{report['corrected weight on the far mode']:.4f} against the posterior "
f"{report['posterior at x=0.7'][1]:.4f}")
# %% 08. The same quantities on the four Gaussian blobs
data = gaussian_mixture_2d(num_points=256, seed=args.seed).double() # [N, 2] the endpoints.
outer_t = 0.6 # The outer time the sampler is conditioned at.
torch.manual_seed(args.seed + 1)
index = torch.randint(0, len(data), (32,))
states = ((1.0 - outer_t) * torch.randn(32, 2) + outer_t * data[index]) # [32, 2] contexts.
states.requires_grad_(True)
exact_weights = posterior_weights(states, outer_t, data) # [32, N] the exact posterior.
reward = data[:, 0] # r(z) = the first coordinate, a reward with a clear direction.
exact_value = torch.log((exact_weights * reward.exp()[None, :]).sum(1)) # [32] V_t(x).
exact_grad = torch.autograd.grad(exact_value.sum(), states, retain_graph=True)[0] # [32, 2].
noise = torch.randn(32, args.samples, 2) # [32, K, 2] inner draws, one per posterior sample.
drawn = inner_map(noise, exact_weights, data, substeps) # [32, K, 2] posterior samples.
sampled_reward = drawn[:, :, 0] # [32, K] the reward at each sample.
estimate = torch.logsumexp(sampled_reward, dim=1) - math.log(args.samples) # [32] V hat.
sample_grad = torch.autograd.grad(estimate.sum(), states, retain_graph=True)[0] # [32, 2].
deterministic = (exact_weights @ reward) # [32] the reward at the posterior mean.
exact_value, estimate = exact_value.detach(), estimate.detach() # Past the two gradients.
deterministic, drawn = deterministic.detach(), drawn.detach()
report["exact V, averaged"] = float(exact_value.mean())
report[f"V from {args.samples} samples"] = float(estimate.mean())
report["single endpoint, averaged"] = float(deterministic.mean())
report["Jensen gap on the blobs"] = float((exact_value - deterministic).mean())
report["posterior mean error"] = float(
(drawn.mean(1) - exact_weights.detach() @ data).norm(dim=1).mean())
report["value gradient error"] = float((sample_grad - exact_grad).norm(dim=1).mean())
report["exact value gradient norm"] = float(exact_grad.norm(dim=1).mean())
states = states.detach()
samples = [f"state ({float(x[0]):+.2f}, {float(x[1]):+.2f}) exact V {float(v):.3f} "
f"{args.samples} samples {float(e):.3f} one endpoint {float(d):.3f}"
for x, v, e, d in zip(states[:4], exact_value[:4], estimate[:4], deterministic[:4])]
print_report("Diamond Maps and Meta Flow Maps, Chapter 6", report, samples)
save_run(args.out, config=vars(args), report=report, samples=samples)
if __name__ == "__main__":
main()
|