"""Optimal-transport conditional flow matching (OT-CFM) on CIFAR-10, CPU-sized. The paper (Appendix C.5) trains "a normalizing flow using optimal transport conditional flow matching (OT-CFM)" following Lipman et al. 2022 / Tong et al. 2023. That algorithm is implemented here rather than substituted for something nearby: * draw a data batch x1 and a noise batch x0 ~ N(0, I); * couple them by EXACT minibatch optimal transport (Hungarian assignment on the squared-Euclidean cost), which is the "OT" in OT-CFM -- plain CFM uses the independent coupling and is a different algorithm; * sample t ~ U(0,1), form x_t = (1-t) x0 + t x1, and regress the network onto the conditional velocity u = x1 - x0; * sample by integrating dx/dt = v(x, t) from t=0 to t=1. The network is a small time-conditioned UNet so the whole loop fits CPU budget. """ from __future__ import annotations import math import numpy as np import torch import torch.nn as nn import torch.nn.functional as F from scipy.optimize import linear_sum_assignment # --------------------------------------------------------------------------- # def timestep_embedding(t: torch.Tensor, dim: int) -> torch.Tensor: half = dim // 2 freqs = torch.exp(-math.log(10000) * torch.arange(half, dtype=torch.float32) / half) args = t[:, None] * freqs[None] return torch.cat([torch.cos(args), torch.sin(args)], dim=-1) class Block(nn.Module): def __init__(self, cin: int, cout: int, tdim: int) -> None: super().__init__() self.c1 = nn.Conv2d(cin, cout, 3, padding=1) self.c2 = nn.Conv2d(cout, cout, 3, padding=1) self.emb = nn.Linear(tdim, cout) self.n1 = nn.GroupNorm(8, cout) self.n2 = nn.GroupNorm(8, cout) self.skip = nn.Conv2d(cin, cout, 1) if cin != cout else nn.Identity() def forward(self, x: torch.Tensor, te: torch.Tensor) -> torch.Tensor: h = F.silu(self.n1(self.c1(x))) h = h + self.emb(te)[:, :, None, None] h = F.silu(self.n2(self.c2(h))) return h + self.skip(x) class UNet(nn.Module): """Small time-conditioned UNet velocity field v(x, t).""" def __init__(self, ch: int = 48, tdim: int = 96, in_ch: int = 3) -> None: super().__init__() self.tdim = tdim self.tmlp = nn.Sequential(nn.Linear(tdim, tdim), nn.SiLU(), nn.Linear(tdim, tdim)) self.d1 = Block(in_ch, ch, tdim) self.d2 = Block(ch, ch * 2, tdim) self.d3 = Block(ch * 2, ch * 2, tdim) self.mid = Block(ch * 2, ch * 2, tdim) self.u3 = Block(ch * 4, ch * 2, tdim) self.u2 = Block(ch * 4, ch, tdim) self.u1 = Block(ch * 2, ch, tdim) self.out = nn.Conv2d(ch, in_ch, 3, padding=1) def forward(self, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor: te = self.tmlp(timestep_embedding(t, self.tdim)) h1 = self.d1(x, te) # 32 h2 = self.d2(F.avg_pool2d(h1, 2), te) # 16 h3 = self.d3(F.avg_pool2d(h2, 2), te) # 8 m = self.mid(h3, te) u3 = self.u3(torch.cat([m, h3], 1), te) u3 = F.interpolate(u3, scale_factor=2, mode="nearest") u2 = self.u2(torch.cat([u3, h2], 1), te) u2 = F.interpolate(u2, scale_factor=2, mode="nearest") u1 = self.u1(torch.cat([u2, h1], 1), te) return self.out(u1) # --------------------------------------------------------------------------- # def ot_pair(x0: torch.Tensor, x1: torch.Tensor) -> torch.Tensor: """Exact minibatch OT coupling: permute x0 to minimise sum ||x0_i - x1_i||^2.""" a = x0.reshape(len(x0), -1) b = x1.reshape(len(x1), -1) cost = torch.cdist(a, b).pow(2).numpy() r, c = linear_sum_assignment(cost) perm = np.empty(len(x0), dtype=np.int64) perm[c] = r return x0[torch.from_numpy(perm)] def cfm_loss(model: UNet, x1: torch.Tensor, generator: torch.Generator) -> torch.Tensor: x0 = torch.randn(x1.shape, generator=generator) x0 = ot_pair(x0, x1) # <-- the OT in OT-CFM t = torch.rand(len(x1), generator=generator) xt = (1 - t[:, None, None, None]) * x0 + t[:, None, None, None] * x1 return F.mse_loss(model(xt, t), x1 - x0) @torch.no_grad() def sample(model: UNet, n: int, steps: int, batch: int, generator: torch.Generator, shape=(3, 32, 32)) -> torch.Tensor: """Euler integration of dx/dt = v(x,t) from t=0 (noise) to t=1.""" model.eval() outs = [] dt = 1.0 / steps for s in range(0, n, batch): b = min(batch, n - s) x = torch.randn((b, *shape), generator=generator) for k in range(steps): t = torch.full((b,), k * dt) x = x + dt * model(x, t) outs.append(x) model.train() return torch.cat(outs)[:n]