CIS6270 / lecture_5 /flows.py
pranamanam's picture
Add Lecture 4 discrete diffusion and Lecture 5 discrete flow matching
c8293a4 verified
Raw
History Blame Contribute Delete
8.42 kB
"""Full teaching samplers for simplex, rectification, and guided jump flows."""
import math
import numpy as np
import torch
import torch.nn.functional as F
from lecture_core import (K, draw, gat_rates, rate_step, gat_sample,
dirichlet_velocity, dna_neighbors, mh_refine)
from common import project_simplex, objectives, soft_objectives, decode
@torch.no_grad()
def simplex_sample(model, method, batch, length, steps=100, a_max=8.,
beta=1., tau_max=4., guidance=0.):
"""Finite Euler discretization with explicit simplex/orthant projection.
Projection is a numerical safeguard, not the exact continuous dynamics.
Gumbel has an approximate finite-temperature base and endpoint.
"""
base = torch.distributions.Dirichlet(torch.ones(K))
z = base.sample((batch, length))
corrections = 0
if method == 'gumbel':
uniform = torch.rand(batch, length, K).clamp(1e-6, 1-1e-6)
g = -(-uniform.log()).log()
z = (g / (beta * tau_max)).softmax(-1)
y = z.sqrt() if method == 'fisher' else z
end = a_max if method == 'dirichlet' else 1.
dt = end / steps
for i in range(steps):
time = i * dt
t = torch.full((batch,), time)
if method == 'dirichlet':
posterior = model(y, t).softmax(-1)
velocity = dirichlet_velocity(y, posterior, time)
elif method == 'fisher':
raw = model(y, t)
velocity = raw - y * (raw * y).sum(-1, keepdim=True)
else:
raw = model(y, t)
velocity = raw - raw.mean(-1, keepdim=True)
if guidance:
# A differentiable toy objective on probabilities, not a trained
# biochemical predictor. Choose a direction toward higher GC.
with torch.enable_grad():
state = y.detach().requires_grad_(True)
probability = state.square() if method == 'fisher' else state
score = soft_objectives(probability)[:, 0].sum()
gradient = torch.autograd.grad(score, state)[0]
if method == 'fisher':
gradient -= y * (gradient * y).sum(-1, keepdim=True)
else:
gradient -= gradient.mean(-1, keepdim=True)
velocity += guidance * gradient
candidate = y + dt * velocity
if not torch.isfinite(candidate).all():
raise FloatingPointError('Nonfinite simplex integration state.')
corrections += int((candidate < 0).any(-1).sum())
if method == 'fisher':
candidate = candidate.clamp_min(1e-6)
y = candidate / candidate.norm(dim=-1, keepdim=True)
else:
y = project_simplex(candidate)
prob = y.square() if method == 'fisher' else y
result = prob.argmax(-1)
return result, {'negative_coordinate_corrections': corrections,
'max_probability_sum_error': float((prob.sum(-1)-1).abs().max()),
'finite_endpoint': True, 'decode': 'argmax'}
def rectified_training_loss(model, clean):
source = torch.distributions.Dirichlet(torch.ones(K)).sample(clean.shape)
target = F.one_hot(clean, K).float()
t = torch.rand(len(clean))
z = (1-t[:, None, None])*source + t[:, None, None]*target
raw = model(z, t)
prediction = raw - raw.mean(-1, keepdim=True)
return (prediction-(target-source)).square().sum(-1).mean()
@torch.no_grad()
def make_teacher_pairs(teacher, count, length, steps=60, chunk=64):
sources, targets = [], []
for start in range(0, count, chunk):
batch = min(chunk, count-start)
source = torch.randint(K, (batch, length))
target = gat_sample(teacher, batch, length, steps, source=source)
sources.append(source); targets.append(target)
return torch.cat(sources), torch.cat(targets)
def mog_multiplier(changes, preference, strength=1., angle=math.pi/3):
"""Rank/direction score and acute-cone test; all objectives maximize."""
from scipy.stats import rankdata
ranks = rankdata(changes, axis=0, method='average') / len(changes)
rank_score = ranks.mean(1)
norm = np.linalg.norm(changes, axis=1)
cosine = (changes @ preference) / np.maximum(norm*np.linalg.norm(preference), 1e-12)
direction = changes @ preference
# Formula matches the lecture: z-standardized rank and direction terms.
def standardize(x):
return (x-x.mean()) / max(float(x.std()), 1e-12)
score = standardize(rank_score) + standardize(direction)
keep = (norm > 0) & (cosine >= np.cos(angle))
return strength * np.exp(np.clip(score, -20, 20)) * keep
@torch.no_grad()
def mog_sample(model, batch, length, steps=50, preference=(.7, .3), strength=1.):
"""Evaluate all single-base edits, reweight rates, then use adaptive Euler.
All-position scoring differs from the paper's random-position loop.
Rank normalization is per position. The adaptive cone stays acute;
no outside-cone fallback is used, so the local theorem remains valid.
"""
z = torch.randint(K, (batch, length))
t = 0.
rejected = 0
angle = math.pi/3
ema = .5
count = 0
while t < 1-1e-6:
tb = torch.full((batch,), t)
rates = gat_rates(model(z, tb).softmax(-1), z, tb)
rejected_step = 0
for b in range(batch):
for i in range(length):
candidates, destinations = [], []
for a in range(K):
if a == int(z[b, i]):
continue
y = z[b].clone(); y[i] = a
candidates.append(y); destinations.append(a)
changes = (objectives(torch.stack(candidates)) - objectives(z[b:b+1])).numpy()
multiplier = mog_multiplier(changes, np.array(preference), strength, angle)
rejected_step += int(np.sum(multiplier == 0))
for a, value in zip(destinations, multiplier):
rates[b, i, a] *= float(value)
rejected += rejected_step
rejection = rejected_step / (batch * length * (K-1))
ema = .9 * ema + .1 * rejection
angle = float(np.clip(angle * np.exp(.02 * (ema-.5)),
np.deg2rad(10), np.deg2rad(89)))
max_exit = float(rates.sum(-1).max())
h = min(1./steps, 1-t, .5/max(max_exit, 1e-8))
z = rate_step(z, rates, h)
t += h
count += 1
if count > 50000:
raise RuntimeError('MOG integration failed to advance.')
return z, {'filtered_candidates': rejected, 'adaptive_steps': count,
'preference': list(preference), 'final_cone_angle_degrees': float(np.rad2deg(angle)),
'guarantee': 'local weighted progress under the acute-cone assumptions'}
def reference_model(data, pseudocount=.5):
"""A tractable positive product reference for exact MH ratios."""
counts = F.one_hot(data, K).float().sum(0) + pseudocount
probability = counts / counts.sum(-1, keepdim=True)
def log_probability(sequence):
idx = torch.tensor(['ACGT'.index(c) for c in sequence])
return float(probability.log()[torch.arange(len(idx)), idx].sum())
return probability, log_probability
def refine_areuredi(tokens, data, steps=100, preference=(.5,.5), eta_max=8., seed=7):
"""ReDi endpoint refinement with normalized proposals and exact MH ratios.
The reference is explicitly fitted and evaluable; it is not a denoiser
joint density. Annealing is finite, so no global-optimum claim is made.
"""
from lecture_core import encode
_, log_ref = reference_model(data)
weight = np.array(preference)
def score(sequence):
values = objectives(encode([sequence]))[0].numpy()
return float(np.min(weight * values))
rng = np.random.default_rng(seed)
strings = decode(tokens)
changes = 0
for step in range(steps):
eta = eta_max * (step+1) / steps
for i, sequence in enumerate(strings):
new = mh_refine(sequence, score, log_ref, eta, rng)
changes += int(new != sequence)
strings[i] = new
return encode(strings), {'accepted_edits': changes, 'eta_final': eta_max,
'reference': 'smoothed position-wise empirical DNA frequencies',
'scope': 'finite annealing demonstration, not an equilibrium certificate'}