funcflows 0.1.2__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- funcflows-0.1.3/PKG-INFO +291 -0
- funcflows-0.1.3/README.md +265 -0
- funcflows-0.1.3/funcflows.egg-info/PKG-INFO +291 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/pyproject.toml +2 -2
- funcflows-0.1.2/PKG-INFO +0 -43
- funcflows-0.1.2/README.md +0 -17
- funcflows-0.1.2/funcflows.egg-info/PKG-INFO +0 -43
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/__init__.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/base_measures/__init__.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/base_measures/abstract_reference_measure.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/base_measures/bases.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/base_measures/gaussian_reference_measure.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/diagnostics/__init__.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/diagnostics/coverage.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/diagnostics/importance.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/objectives/__init__.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/objectives/alpha_divergence.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/objectives/flow_matching.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/objectives/negative_logl.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/objectives/reverse_kl.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/samplers/__init__.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/samplers/latent_pcn.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/__init__.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/abstract_transformation.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/continuous/__init__.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/continuous/base_continuous.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/continuous/conditioners.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/continuous/grid_fields.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/continuous/vector_fields.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/layers/__init__.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/layers/base_discrete.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/layers/layer_classes.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/utils/__init__.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/utils/gaussian_misfit.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/utils/train.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/LICENSE +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/funcflows.egg-info/SOURCES.txt +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/funcflows.egg-info/dependency_links.txt +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/funcflows.egg-info/requires.txt +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/funcflows.egg-info/top_level.txt +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/setup.cfg +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/tests/test_basis.py +0 -0
- {funcflows-0.1.2 → funcflows-0.1.3}/tests/test_measures.py +0 -0
funcflows-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: funcflows
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Normalizing flows and flow matching on function spaces, with exact-trace vector fields
|
|
5
|
+
Author: Liam Pinchbeck
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/funcflows/
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: torch
|
|
17
|
+
Requires-Dist: torchvision
|
|
18
|
+
Requires-Dist: tqdm
|
|
19
|
+
Requires-Dist: scipy
|
|
20
|
+
Requires-Dist: matplotlib
|
|
21
|
+
Provides-Extra: ot
|
|
22
|
+
Requires-Dist: scipy; extra == "ot"
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest; extra == "test"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# FuncFlows
|
|
28
|
+
|
|
29
|
+
Normalizing flows and flow matching on function spaces.
|
|
30
|
+
|
|
31
|
+
A function is represented by its coefficients on a Laplacian eigenbasis (`CosineBasis` or
|
|
32
|
+
`FourierBasis`), the reference measure is a Gaussian on those coefficients, and a transport is a
|
|
33
|
+
neural ODE in coefficient space. The vector fields (`LinearField`, `MatrixField`) have closed-form
|
|
34
|
+
divergences, so the density of the transported measure relative to the
|
|
35
|
+
reference measure is exact — no Hutchinson estimators — which makes reverse-KL training,
|
|
36
|
+
likelihood training and importance reweighting usable at hundreds of modes.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install funcflows # torch + tqdm
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`torch` is large; if you need a specific CUDA/MPS build install it first from pytorch.org.
|
|
43
|
+
|
|
44
|
+
`examples/quickstart.py` runs both patterns below end to end in a couple of minutes on CPU.
|
|
45
|
+
|
|
46
|
+
## The pieces
|
|
47
|
+
|
|
48
|
+
| Piece | Role |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `CosineBasis(M)`, `FourierBasis(M)` | `M` orthonormal basis functions on [0, 1]; `basis.evaluate(points)` → design matrix |
|
|
51
|
+
| `GaussianReferenceMeasure(basis, alpha, power)` or `(basis, variances=...)` | base measure N(0, diag σ²) on the coefficients; `.sample(n)`, `.scale` (= σ) |
|
|
52
|
+
| `LinearField` | per-mode gain plus a data-dependent drift; exact for the linear-Gaussian part |
|
|
53
|
+
| `MatrixField(conditioner)` | one wide tanh layer over all coefficients; global nonlinear mixing |
|
|
54
|
+
| `SumField(*fields)` | add fields; traces add |
|
|
55
|
+
| `ContinuousTransformation(measure, field, num_steps)` | the flow: `.transport(v0)`, `.push_forward`, `.pull_back`, `.log_rn_at(v)` |
|
|
56
|
+
| `FlowMatching`, `ConditionalFlowMatching`, `ReverseKL`, `NegativeLogL` | objectives; each is a callable returning a loss |
|
|
57
|
+
| `train(objective, params, num_steps, learning_rate)` | Adam loop, returns the loss history |
|
|
58
|
+
| `ImportanceCorrection`, `coverage_curve`, `latent_pcn` | diagnostics and an exact latent-space MCMC sampler |
|
|
59
|
+
|
|
60
|
+
Every field takes `mode_scale=measure.scale`: the nonlinearity sees whitened coefficients and the
|
|
61
|
+
divergence is unchanged (similarity transform). Always pass it.
|
|
62
|
+
|
|
63
|
+
## 1. Learn a prior from samples (flow matching)
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import torch
|
|
67
|
+
from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
|
|
68
|
+
from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
|
|
69
|
+
MatrixField, TimeBasisConditioner)
|
|
70
|
+
from FuncFlows.objectives import FlowMatching
|
|
71
|
+
from FuncFlows.utils.train import train
|
|
72
|
+
|
|
73
|
+
M = 64
|
|
74
|
+
basis = CosineBasis(M)
|
|
75
|
+
coeffs = ... # [N, M] training functions projected onto the basis
|
|
76
|
+
measure = GaussianReferenceMeasure(basis, variances=coeffs.var(0)) # diagonal Gaussian fit = the GP baseline
|
|
77
|
+
|
|
78
|
+
field = SumField(
|
|
79
|
+
LinearField(M, num_time_modes=4),
|
|
80
|
+
MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=measure.scale))
|
|
81
|
+
flow = ContinuousTransformation(measure, field, num_steps=16)
|
|
82
|
+
|
|
83
|
+
losses = train(FlowMatching(flow, coeffs, batch_size=256, weights=1 / measure.scale, coupling="optimal"),
|
|
84
|
+
flow.parameters(), num_steps=10000, learning_rate=2e-3)
|
|
85
|
+
|
|
86
|
+
draws = flow.transport(measure.sample(1000)) # [1000, M] new functions
|
|
87
|
+
points = torch.linspace(0, 1, 400).reshape(-1, 1)
|
|
88
|
+
values = draws @ basis.evaluate(points).T # [1000, 400] on a grid
|
|
89
|
+
log_q = flow.log_rn_at(draws) # exact log density w.r.t. the reference measure
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`weights=1/measure.scale` whitens the loss so high modes count; `coupling="optimal"` needs
|
|
93
|
+
scipy and only makes sense for an unconditional flow.
|
|
94
|
+
|
|
95
|
+
## 2. Posterior from a likelihood (no samples needed)
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from FuncFlows.objectives import ReverseKL
|
|
99
|
+
from FuncFlows.utils.gaussian_misfit import GaussianMisfit
|
|
100
|
+
from FuncFlows.diagnostics import ImportanceCorrection
|
|
101
|
+
|
|
102
|
+
prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0) # Matérn-like spectrum
|
|
103
|
+
phi_obs = basis.evaluate(obs_points) # [n_obs, M]
|
|
104
|
+
forward = lambda v: (v @ phi_obs.T) ** 2 # any differentiable map
|
|
105
|
+
potential = GaussianMisfit(forward, data, noise_std) # -log likelihood, [batch] -> [batch]
|
|
106
|
+
|
|
107
|
+
flow = ContinuousTransformation(prior, field, num_steps=16) # same field recipe as above
|
|
108
|
+
train(ReverseKL(flow, potential, num_samples=64, path_gradient=True), flow.parameters(), num_steps=3000)
|
|
109
|
+
|
|
110
|
+
draws = flow.transport(prior.sample(2000))
|
|
111
|
+
check = ImportanceCorrection(flow, draws, potential) # exact because the traces are exact
|
|
112
|
+
print(check.efficiency, check.log_evidence) # efficiency near 1 = posterior matched
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
For an exact sampler on top of a learned prior use `latent_pcn(flow, potential)`: pCN in the flow's
|
|
116
|
+
latent space, whose acceptance needs no Jacobian.
|
|
117
|
+
|
|
118
|
+
## 3. Amortised posterior (condition on data)
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from FuncFlows.transports.continuous import DataConditioner
|
|
122
|
+
from FuncFlows.objectives import ConditionalFlowMatching
|
|
123
|
+
|
|
124
|
+
C = ... # context dimension (e.g. whitened projected data)
|
|
125
|
+
field = SumField(
|
|
126
|
+
LinearField(M, C, num_time_modes=4, mode_scale=measure.scale), # data drift: the linear-Gaussian mean
|
|
127
|
+
MatrixField(DataConditioner(M, 256, C, num_time_modes=4), mode_scale=measure.scale))
|
|
128
|
+
flow = ContinuousTransformation(measure, field, num_steps=16)
|
|
129
|
+
|
|
130
|
+
def simulate(batch): # -> (target coeffs [batch, M], context [batch, C])
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
train(ConditionalFlowMatching(flow, simulate, measure.sample, batch_size=256, weights=1 / measure.scale),
|
|
134
|
+
flow.parameters(), num_steps=20000, learning_rate=2e-3)
|
|
135
|
+
|
|
136
|
+
posterior = flow.transport(measure.sample(1000), context.expand(1000, -1)) # one ODE solve per posterior
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Whiten the context by `measure.scale` when it is itself a projected function; otherwise the drift
|
|
140
|
+
weights for high modes have to reach 1/σ and never do. When a conjugate (linear-Gaussian)
|
|
141
|
+
posterior is available in closed form, use it as the base measure and let the flow learn the residual.
|
|
142
|
+
|
|
143
|
+
## Diagnostics
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from FuncFlows.diagnostics import coverage_curve, coverage_error
|
|
147
|
+
levels, coverage = coverage_curve(samples, truths, weights=1 / measure.scale) # TARP; samples [cases, draws, M]
|
|
148
|
+
coverage_error(levels, coverage) # signed max gap from the diagonal: negative = overconfident
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Layout
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
FuncFlows/
|
|
155
|
+
base_measures/ bases, GaussianReferenceMeasure
|
|
156
|
+
transports/ continuous/: fields, conditioners, ContinuousTransformation
|
|
157
|
+
(grid_fields.py: PointwiseField / OperatorField, spatial layers — experimental)
|
|
158
|
+
objectives/ flow_matching, reverse_kl, negative_logl
|
|
159
|
+
samplers/ latent_pcn
|
|
160
|
+
diagnostics/ importance, coverage
|
|
161
|
+
utils/ train, gaussian_misfit
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Full Example
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import matplotlib
|
|
171
|
+
matplotlib.use("Agg")
|
|
172
|
+
import matplotlib.pyplot as plt
|
|
173
|
+
import torch
|
|
174
|
+
|
|
175
|
+
from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
|
|
176
|
+
from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
|
|
177
|
+
MatrixField, TimeBasisConditioner)
|
|
178
|
+
from FuncFlows.objectives import ReverseKL, FlowMatching
|
|
179
|
+
from FuncFlows.samplers import latent_pcn
|
|
180
|
+
from FuncFlows.utils.train import train
|
|
181
|
+
from FuncFlows.utils.gaussian_misfit import GaussianMisfit
|
|
182
|
+
from FuncFlows.diagnostics import ImportanceCorrection
|
|
183
|
+
|
|
184
|
+
torch.manual_seed(1)
|
|
185
|
+
M, NUM_OBS, NOISE, N = 32, 15, 0.05, 2000
|
|
186
|
+
basis = CosineBasis(M)
|
|
187
|
+
prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0)
|
|
188
|
+
x = torch.linspace(0, 1, 400, dtype=torch.float64)[:, None]
|
|
189
|
+
design = basis.evaluate(x)
|
|
190
|
+
|
|
191
|
+
# data || y = f(x)^2 + noise at NUM_OBS random points
|
|
192
|
+
truth = prior.sample(1)
|
|
193
|
+
obs_x = torch.rand(NUM_OBS, 1, dtype=torch.float64)
|
|
194
|
+
phi_obs = basis.evaluate(obs_x)
|
|
195
|
+
forward = lambda v: (v @ phi_obs.T) ** 2
|
|
196
|
+
data = forward(truth)[0] + NOISE * torch.randn(NUM_OBS, dtype=torch.float64)
|
|
197
|
+
misfit = GaussianMisfit(forward, data, NOISE) # -logl
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def make_flow():
|
|
201
|
+
field = SumField(LinearField(M, num_time_modes=4),
|
|
202
|
+
MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=prior.scale))
|
|
203
|
+
return ContinuousTransformation(prior, field, num_steps=16)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@torch.no_grad()
|
|
207
|
+
def pcn(potential, num_chains=64, num_steps=20000, beta=0.15, burn=5000, thin=20):
|
|
208
|
+
state, keep = prior.sample(num_chains), []
|
|
209
|
+
energy = potential(state)
|
|
210
|
+
for step in range(num_steps):
|
|
211
|
+
proposal = (1 - beta ** 2) ** 0.5 * state + beta * prior.sample(num_chains)
|
|
212
|
+
proposal_energy = potential(proposal)
|
|
213
|
+
accept = torch.log(torch.rand_like(energy)) < energy - proposal_energy
|
|
214
|
+
state = torch.where(accept[:, None], proposal, state)
|
|
215
|
+
energy = torch.where(accept, proposal_energy, energy)
|
|
216
|
+
if step >= burn and step % thin == 0:
|
|
217
|
+
keep.append(state.clone())
|
|
218
|
+
return torch.cat(keep)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def laplace_gp():
|
|
222
|
+
white = torch.zeros(M, dtype=torch.float64, requires_grad=True)
|
|
223
|
+
opt = torch.optim.LBFGS([white], max_iter=200, line_search_fn="strong_wolfe")
|
|
224
|
+
|
|
225
|
+
def closure():
|
|
226
|
+
opt.zero_grad()
|
|
227
|
+
loss = misfit(white * prior.scale) + 0.5 * (white ** 2).sum()
|
|
228
|
+
loss.backward()
|
|
229
|
+
return loss
|
|
230
|
+
|
|
231
|
+
opt.step(closure)
|
|
232
|
+
with torch.no_grad():
|
|
233
|
+
jac = 2 * (phi_obs @ (white * prior.scale))[:, None] * phi_obs * prior.scale
|
|
234
|
+
cov = torch.linalg.inv(torch.eye(M, dtype=torch.float64) + jac.T @ jac / NOISE ** 2)
|
|
235
|
+
return (white + torch.randn(N, M, dtype=torch.float64) @ torch.linalg.cholesky(cov).T) * prior.scale
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# run everything
|
|
239
|
+
results = {}
|
|
240
|
+
results["pCN reference"] = pcn(misfit)[torch.randperm(64 * 750)[:N]] # thin to N for the comparisons
|
|
241
|
+
|
|
242
|
+
results["GP (Laplace)"] = laplace_gp()
|
|
243
|
+
|
|
244
|
+
rkl = make_flow()
|
|
245
|
+
train(ReverseKL(rkl, misfit, num_samples=64, path_gradient=True), rkl.parameters(), num_steps=2500, learning_rate=3e-3)
|
|
246
|
+
with torch.no_grad():
|
|
247
|
+
results["reverse KL"] = rkl.transport(prior.sample(N))
|
|
248
|
+
|
|
249
|
+
fm = make_flow()
|
|
250
|
+
train(FlowMatching(fm, results["pCN reference"], batch_size=256), fm.parameters(), num_steps=4000, learning_rate=3e-3)
|
|
251
|
+
with torch.no_grad():
|
|
252
|
+
results["flow matching"] = fm.transport(prior.sample(N))
|
|
253
|
+
|
|
254
|
+
# pCN in the latent space of the posterior flow: target exp(-Phi(Tz)) prior(Tz)/q(Tz) mu0(dz), so add the
|
|
255
|
+
# flow's exact log-density to the potential. A good flow makes the target nearly flat -> big steps accepted.
|
|
256
|
+
corrected = lambda v: misfit(v) + fm.log_rn_at(v) # noqa: E731
|
|
257
|
+
draws, info = latent_pcn(fm, corrected, num_chains=128, num_steps=1500, beta=0.5, thin=10)
|
|
258
|
+
results["latent pCN"] = draws[torch.randperm(len(draws))[:N]]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# report
|
|
263
|
+
ref = results["pCN reference"] @ design.T
|
|
264
|
+
print(f"\n{'method':16s} {'+sign':>6s} {'E-dist':>8s} (ideal 0.50; distance to pCN)")
|
|
265
|
+
for name, v in results.items():
|
|
266
|
+
values = v @ design.T
|
|
267
|
+
sign = ((v @ truth[0]) > 0).double().mean().item()
|
|
268
|
+
dist = (2 * torch.cdist(values, ref).mean() - torch.cdist(values, values).mean() - torch.cdist(ref, ref).mean()) / 20
|
|
269
|
+
print(f"{name:16s} {sign:6.2f} {dist.item():8.4f}")
|
|
270
|
+
for name, flow in [("reverse KL", rkl), ("flow matching", fm)]:
|
|
271
|
+
with torch.no_grad():
|
|
272
|
+
c = ImportanceCorrection(flow, results[name], misfit)
|
|
273
|
+
print(f"{name}: importance efficiency {c.efficiency:.3f}, log evidence {c.log_evidence:.2f}")
|
|
274
|
+
print(f"latent pCN acceptance {info['acceptance']:.2f} at beta 0.5 (plain pCN would be ~0)")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# figures
|
|
278
|
+
fig, axes = plt.subplots(1, len(results), figsize=(3.6 * len(results), 3.2), sharey=True)
|
|
279
|
+
tv = (truth @ design.T)[0]
|
|
280
|
+
for ax, (name, v) in zip(axes, results.items()):
|
|
281
|
+
ax.plot(x[:, 0], (v[:40] @ design.T).T, color="C0", alpha=0.15, lw=1)
|
|
282
|
+
ax.plot(x[:, 0], tv, "k", lw=1.5)
|
|
283
|
+
ax.plot(x[:, 0], -tv, "k--", lw=1.5)
|
|
284
|
+
ax.scatter(obs_x[:, 0], data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
|
|
285
|
+
ax.scatter(obs_x[:, 0], -data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
|
|
286
|
+
ax.set_title(name, fontsize=10)
|
|
287
|
+
fig.suptitle("y = f(x)^2 + noise: black = ±truth, red = ±sqrt(data), blue = posterior draws", fontsize=10)
|
|
288
|
+
fig.tight_layout()
|
|
289
|
+
fig.savefig("bimodal_posterior.png", dpi=130)
|
|
290
|
+
print("saved bimodal_posterior.png")
|
|
291
|
+
```
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# FuncFlows
|
|
2
|
+
|
|
3
|
+
Normalizing flows and flow matching on function spaces.
|
|
4
|
+
|
|
5
|
+
A function is represented by its coefficients on a Laplacian eigenbasis (`CosineBasis` or
|
|
6
|
+
`FourierBasis`), the reference measure is a Gaussian on those coefficients, and a transport is a
|
|
7
|
+
neural ODE in coefficient space. The vector fields (`LinearField`, `MatrixField`) have closed-form
|
|
8
|
+
divergences, so the density of the transported measure relative to the
|
|
9
|
+
reference measure is exact — no Hutchinson estimators — which makes reverse-KL training,
|
|
10
|
+
likelihood training and importance reweighting usable at hundreds of modes.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install funcflows # torch + tqdm
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`torch` is large; if you need a specific CUDA/MPS build install it first from pytorch.org.
|
|
17
|
+
|
|
18
|
+
`examples/quickstart.py` runs both patterns below end to end in a couple of minutes on CPU.
|
|
19
|
+
|
|
20
|
+
## The pieces
|
|
21
|
+
|
|
22
|
+
| Piece | Role |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `CosineBasis(M)`, `FourierBasis(M)` | `M` orthonormal basis functions on [0, 1]; `basis.evaluate(points)` → design matrix |
|
|
25
|
+
| `GaussianReferenceMeasure(basis, alpha, power)` or `(basis, variances=...)` | base measure N(0, diag σ²) on the coefficients; `.sample(n)`, `.scale` (= σ) |
|
|
26
|
+
| `LinearField` | per-mode gain plus a data-dependent drift; exact for the linear-Gaussian part |
|
|
27
|
+
| `MatrixField(conditioner)` | one wide tanh layer over all coefficients; global nonlinear mixing |
|
|
28
|
+
| `SumField(*fields)` | add fields; traces add |
|
|
29
|
+
| `ContinuousTransformation(measure, field, num_steps)` | the flow: `.transport(v0)`, `.push_forward`, `.pull_back`, `.log_rn_at(v)` |
|
|
30
|
+
| `FlowMatching`, `ConditionalFlowMatching`, `ReverseKL`, `NegativeLogL` | objectives; each is a callable returning a loss |
|
|
31
|
+
| `train(objective, params, num_steps, learning_rate)` | Adam loop, returns the loss history |
|
|
32
|
+
| `ImportanceCorrection`, `coverage_curve`, `latent_pcn` | diagnostics and an exact latent-space MCMC sampler |
|
|
33
|
+
|
|
34
|
+
Every field takes `mode_scale=measure.scale`: the nonlinearity sees whitened coefficients and the
|
|
35
|
+
divergence is unchanged (similarity transform). Always pass it.
|
|
36
|
+
|
|
37
|
+
## 1. Learn a prior from samples (flow matching)
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import torch
|
|
41
|
+
from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
|
|
42
|
+
from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
|
|
43
|
+
MatrixField, TimeBasisConditioner)
|
|
44
|
+
from FuncFlows.objectives import FlowMatching
|
|
45
|
+
from FuncFlows.utils.train import train
|
|
46
|
+
|
|
47
|
+
M = 64
|
|
48
|
+
basis = CosineBasis(M)
|
|
49
|
+
coeffs = ... # [N, M] training functions projected onto the basis
|
|
50
|
+
measure = GaussianReferenceMeasure(basis, variances=coeffs.var(0)) # diagonal Gaussian fit = the GP baseline
|
|
51
|
+
|
|
52
|
+
field = SumField(
|
|
53
|
+
LinearField(M, num_time_modes=4),
|
|
54
|
+
MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=measure.scale))
|
|
55
|
+
flow = ContinuousTransformation(measure, field, num_steps=16)
|
|
56
|
+
|
|
57
|
+
losses = train(FlowMatching(flow, coeffs, batch_size=256, weights=1 / measure.scale, coupling="optimal"),
|
|
58
|
+
flow.parameters(), num_steps=10000, learning_rate=2e-3)
|
|
59
|
+
|
|
60
|
+
draws = flow.transport(measure.sample(1000)) # [1000, M] new functions
|
|
61
|
+
points = torch.linspace(0, 1, 400).reshape(-1, 1)
|
|
62
|
+
values = draws @ basis.evaluate(points).T # [1000, 400] on a grid
|
|
63
|
+
log_q = flow.log_rn_at(draws) # exact log density w.r.t. the reference measure
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`weights=1/measure.scale` whitens the loss so high modes count; `coupling="optimal"` needs
|
|
67
|
+
scipy and only makes sense for an unconditional flow.
|
|
68
|
+
|
|
69
|
+
## 2. Posterior from a likelihood (no samples needed)
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from FuncFlows.objectives import ReverseKL
|
|
73
|
+
from FuncFlows.utils.gaussian_misfit import GaussianMisfit
|
|
74
|
+
from FuncFlows.diagnostics import ImportanceCorrection
|
|
75
|
+
|
|
76
|
+
prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0) # Matérn-like spectrum
|
|
77
|
+
phi_obs = basis.evaluate(obs_points) # [n_obs, M]
|
|
78
|
+
forward = lambda v: (v @ phi_obs.T) ** 2 # any differentiable map
|
|
79
|
+
potential = GaussianMisfit(forward, data, noise_std) # -log likelihood, [batch] -> [batch]
|
|
80
|
+
|
|
81
|
+
flow = ContinuousTransformation(prior, field, num_steps=16) # same field recipe as above
|
|
82
|
+
train(ReverseKL(flow, potential, num_samples=64, path_gradient=True), flow.parameters(), num_steps=3000)
|
|
83
|
+
|
|
84
|
+
draws = flow.transport(prior.sample(2000))
|
|
85
|
+
check = ImportanceCorrection(flow, draws, potential) # exact because the traces are exact
|
|
86
|
+
print(check.efficiency, check.log_evidence) # efficiency near 1 = posterior matched
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For an exact sampler on top of a learned prior use `latent_pcn(flow, potential)`: pCN in the flow's
|
|
90
|
+
latent space, whose acceptance needs no Jacobian.
|
|
91
|
+
|
|
92
|
+
## 3. Amortised posterior (condition on data)
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from FuncFlows.transports.continuous import DataConditioner
|
|
96
|
+
from FuncFlows.objectives import ConditionalFlowMatching
|
|
97
|
+
|
|
98
|
+
C = ... # context dimension (e.g. whitened projected data)
|
|
99
|
+
field = SumField(
|
|
100
|
+
LinearField(M, C, num_time_modes=4, mode_scale=measure.scale), # data drift: the linear-Gaussian mean
|
|
101
|
+
MatrixField(DataConditioner(M, 256, C, num_time_modes=4), mode_scale=measure.scale))
|
|
102
|
+
flow = ContinuousTransformation(measure, field, num_steps=16)
|
|
103
|
+
|
|
104
|
+
def simulate(batch): # -> (target coeffs [batch, M], context [batch, C])
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
train(ConditionalFlowMatching(flow, simulate, measure.sample, batch_size=256, weights=1 / measure.scale),
|
|
108
|
+
flow.parameters(), num_steps=20000, learning_rate=2e-3)
|
|
109
|
+
|
|
110
|
+
posterior = flow.transport(measure.sample(1000), context.expand(1000, -1)) # one ODE solve per posterior
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Whiten the context by `measure.scale` when it is itself a projected function; otherwise the drift
|
|
114
|
+
weights for high modes have to reach 1/σ and never do. When a conjugate (linear-Gaussian)
|
|
115
|
+
posterior is available in closed form, use it as the base measure and let the flow learn the residual.
|
|
116
|
+
|
|
117
|
+
## Diagnostics
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from FuncFlows.diagnostics import coverage_curve, coverage_error
|
|
121
|
+
levels, coverage = coverage_curve(samples, truths, weights=1 / measure.scale) # TARP; samples [cases, draws, M]
|
|
122
|
+
coverage_error(levels, coverage) # signed max gap from the diagonal: negative = overconfident
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Layout
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
FuncFlows/
|
|
129
|
+
base_measures/ bases, GaussianReferenceMeasure
|
|
130
|
+
transports/ continuous/: fields, conditioners, ContinuousTransformation
|
|
131
|
+
(grid_fields.py: PointwiseField / OperatorField, spatial layers — experimental)
|
|
132
|
+
objectives/ flow_matching, reverse_kl, negative_logl
|
|
133
|
+
samplers/ latent_pcn
|
|
134
|
+
diagnostics/ importance, coverage
|
|
135
|
+
utils/ train, gaussian_misfit
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Full Example
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
import matplotlib
|
|
145
|
+
matplotlib.use("Agg")
|
|
146
|
+
import matplotlib.pyplot as plt
|
|
147
|
+
import torch
|
|
148
|
+
|
|
149
|
+
from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
|
|
150
|
+
from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
|
|
151
|
+
MatrixField, TimeBasisConditioner)
|
|
152
|
+
from FuncFlows.objectives import ReverseKL, FlowMatching
|
|
153
|
+
from FuncFlows.samplers import latent_pcn
|
|
154
|
+
from FuncFlows.utils.train import train
|
|
155
|
+
from FuncFlows.utils.gaussian_misfit import GaussianMisfit
|
|
156
|
+
from FuncFlows.diagnostics import ImportanceCorrection
|
|
157
|
+
|
|
158
|
+
torch.manual_seed(1)
|
|
159
|
+
M, NUM_OBS, NOISE, N = 32, 15, 0.05, 2000
|
|
160
|
+
basis = CosineBasis(M)
|
|
161
|
+
prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0)
|
|
162
|
+
x = torch.linspace(0, 1, 400, dtype=torch.float64)[:, None]
|
|
163
|
+
design = basis.evaluate(x)
|
|
164
|
+
|
|
165
|
+
# data || y = f(x)^2 + noise at NUM_OBS random points
|
|
166
|
+
truth = prior.sample(1)
|
|
167
|
+
obs_x = torch.rand(NUM_OBS, 1, dtype=torch.float64)
|
|
168
|
+
phi_obs = basis.evaluate(obs_x)
|
|
169
|
+
forward = lambda v: (v @ phi_obs.T) ** 2
|
|
170
|
+
data = forward(truth)[0] + NOISE * torch.randn(NUM_OBS, dtype=torch.float64)
|
|
171
|
+
misfit = GaussianMisfit(forward, data, NOISE) # -logl
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def make_flow():
|
|
175
|
+
field = SumField(LinearField(M, num_time_modes=4),
|
|
176
|
+
MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=prior.scale))
|
|
177
|
+
return ContinuousTransformation(prior, field, num_steps=16)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@torch.no_grad()
|
|
181
|
+
def pcn(potential, num_chains=64, num_steps=20000, beta=0.15, burn=5000, thin=20):
|
|
182
|
+
state, keep = prior.sample(num_chains), []
|
|
183
|
+
energy = potential(state)
|
|
184
|
+
for step in range(num_steps):
|
|
185
|
+
proposal = (1 - beta ** 2) ** 0.5 * state + beta * prior.sample(num_chains)
|
|
186
|
+
proposal_energy = potential(proposal)
|
|
187
|
+
accept = torch.log(torch.rand_like(energy)) < energy - proposal_energy
|
|
188
|
+
state = torch.where(accept[:, None], proposal, state)
|
|
189
|
+
energy = torch.where(accept, proposal_energy, energy)
|
|
190
|
+
if step >= burn and step % thin == 0:
|
|
191
|
+
keep.append(state.clone())
|
|
192
|
+
return torch.cat(keep)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def laplace_gp():
|
|
196
|
+
white = torch.zeros(M, dtype=torch.float64, requires_grad=True)
|
|
197
|
+
opt = torch.optim.LBFGS([white], max_iter=200, line_search_fn="strong_wolfe")
|
|
198
|
+
|
|
199
|
+
def closure():
|
|
200
|
+
opt.zero_grad()
|
|
201
|
+
loss = misfit(white * prior.scale) + 0.5 * (white ** 2).sum()
|
|
202
|
+
loss.backward()
|
|
203
|
+
return loss
|
|
204
|
+
|
|
205
|
+
opt.step(closure)
|
|
206
|
+
with torch.no_grad():
|
|
207
|
+
jac = 2 * (phi_obs @ (white * prior.scale))[:, None] * phi_obs * prior.scale
|
|
208
|
+
cov = torch.linalg.inv(torch.eye(M, dtype=torch.float64) + jac.T @ jac / NOISE ** 2)
|
|
209
|
+
return (white + torch.randn(N, M, dtype=torch.float64) @ torch.linalg.cholesky(cov).T) * prior.scale
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# run everything
|
|
213
|
+
results = {}
|
|
214
|
+
results["pCN reference"] = pcn(misfit)[torch.randperm(64 * 750)[:N]] # thin to N for the comparisons
|
|
215
|
+
|
|
216
|
+
results["GP (Laplace)"] = laplace_gp()
|
|
217
|
+
|
|
218
|
+
rkl = make_flow()
|
|
219
|
+
train(ReverseKL(rkl, misfit, num_samples=64, path_gradient=True), rkl.parameters(), num_steps=2500, learning_rate=3e-3)
|
|
220
|
+
with torch.no_grad():
|
|
221
|
+
results["reverse KL"] = rkl.transport(prior.sample(N))
|
|
222
|
+
|
|
223
|
+
fm = make_flow()
|
|
224
|
+
train(FlowMatching(fm, results["pCN reference"], batch_size=256), fm.parameters(), num_steps=4000, learning_rate=3e-3)
|
|
225
|
+
with torch.no_grad():
|
|
226
|
+
results["flow matching"] = fm.transport(prior.sample(N))
|
|
227
|
+
|
|
228
|
+
# pCN in the latent space of the posterior flow: target exp(-Phi(Tz)) prior(Tz)/q(Tz) mu0(dz), so add the
|
|
229
|
+
# flow's exact log-density to the potential. A good flow makes the target nearly flat -> big steps accepted.
|
|
230
|
+
corrected = lambda v: misfit(v) + fm.log_rn_at(v) # noqa: E731
|
|
231
|
+
draws, info = latent_pcn(fm, corrected, num_chains=128, num_steps=1500, beta=0.5, thin=10)
|
|
232
|
+
results["latent pCN"] = draws[torch.randperm(len(draws))[:N]]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
# report
|
|
237
|
+
ref = results["pCN reference"] @ design.T
|
|
238
|
+
print(f"\n{'method':16s} {'+sign':>6s} {'E-dist':>8s} (ideal 0.50; distance to pCN)")
|
|
239
|
+
for name, v in results.items():
|
|
240
|
+
values = v @ design.T
|
|
241
|
+
sign = ((v @ truth[0]) > 0).double().mean().item()
|
|
242
|
+
dist = (2 * torch.cdist(values, ref).mean() - torch.cdist(values, values).mean() - torch.cdist(ref, ref).mean()) / 20
|
|
243
|
+
print(f"{name:16s} {sign:6.2f} {dist.item():8.4f}")
|
|
244
|
+
for name, flow in [("reverse KL", rkl), ("flow matching", fm)]:
|
|
245
|
+
with torch.no_grad():
|
|
246
|
+
c = ImportanceCorrection(flow, results[name], misfit)
|
|
247
|
+
print(f"{name}: importance efficiency {c.efficiency:.3f}, log evidence {c.log_evidence:.2f}")
|
|
248
|
+
print(f"latent pCN acceptance {info['acceptance']:.2f} at beta 0.5 (plain pCN would be ~0)")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# figures
|
|
252
|
+
fig, axes = plt.subplots(1, len(results), figsize=(3.6 * len(results), 3.2), sharey=True)
|
|
253
|
+
tv = (truth @ design.T)[0]
|
|
254
|
+
for ax, (name, v) in zip(axes, results.items()):
|
|
255
|
+
ax.plot(x[:, 0], (v[:40] @ design.T).T, color="C0", alpha=0.15, lw=1)
|
|
256
|
+
ax.plot(x[:, 0], tv, "k", lw=1.5)
|
|
257
|
+
ax.plot(x[:, 0], -tv, "k--", lw=1.5)
|
|
258
|
+
ax.scatter(obs_x[:, 0], data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
|
|
259
|
+
ax.scatter(obs_x[:, 0], -data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
|
|
260
|
+
ax.set_title(name, fontsize=10)
|
|
261
|
+
fig.suptitle("y = f(x)^2 + noise: black = ±truth, red = ±sqrt(data), blue = posterior draws", fontsize=10)
|
|
262
|
+
fig.tight_layout()
|
|
263
|
+
fig.savefig("bimodal_posterior.png", dpi=130)
|
|
264
|
+
print("saved bimodal_posterior.png")
|
|
265
|
+
```
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: funcflows
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Normalizing flows and flow matching on function spaces, with exact-trace vector fields
|
|
5
|
+
Author: Liam Pinchbeck
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/funcflows/
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: torch
|
|
17
|
+
Requires-Dist: torchvision
|
|
18
|
+
Requires-Dist: tqdm
|
|
19
|
+
Requires-Dist: scipy
|
|
20
|
+
Requires-Dist: matplotlib
|
|
21
|
+
Provides-Extra: ot
|
|
22
|
+
Requires-Dist: scipy; extra == "ot"
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest; extra == "test"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# FuncFlows
|
|
28
|
+
|
|
29
|
+
Normalizing flows and flow matching on function spaces.
|
|
30
|
+
|
|
31
|
+
A function is represented by its coefficients on a Laplacian eigenbasis (`CosineBasis` or
|
|
32
|
+
`FourierBasis`), the reference measure is a Gaussian on those coefficients, and a transport is a
|
|
33
|
+
neural ODE in coefficient space. The vector fields (`LinearField`, `MatrixField`) have closed-form
|
|
34
|
+
divergences, so the density of the transported measure relative to the
|
|
35
|
+
reference measure is exact — no Hutchinson estimators — which makes reverse-KL training,
|
|
36
|
+
likelihood training and importance reweighting usable at hundreds of modes.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install funcflows # torch + tqdm
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
`torch` is large; if you need a specific CUDA/MPS build install it first from pytorch.org.
|
|
43
|
+
|
|
44
|
+
`examples/quickstart.py` runs both patterns below end to end in a couple of minutes on CPU.
|
|
45
|
+
|
|
46
|
+
## The pieces
|
|
47
|
+
|
|
48
|
+
| Piece | Role |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `CosineBasis(M)`, `FourierBasis(M)` | `M` orthonormal basis functions on [0, 1]; `basis.evaluate(points)` → design matrix |
|
|
51
|
+
| `GaussianReferenceMeasure(basis, alpha, power)` or `(basis, variances=...)` | base measure N(0, diag σ²) on the coefficients; `.sample(n)`, `.scale` (= σ) |
|
|
52
|
+
| `LinearField` | per-mode gain plus a data-dependent drift; exact for the linear-Gaussian part |
|
|
53
|
+
| `MatrixField(conditioner)` | one wide tanh layer over all coefficients; global nonlinear mixing |
|
|
54
|
+
| `SumField(*fields)` | add fields; traces add |
|
|
55
|
+
| `ContinuousTransformation(measure, field, num_steps)` | the flow: `.transport(v0)`, `.push_forward`, `.pull_back`, `.log_rn_at(v)` |
|
|
56
|
+
| `FlowMatching`, `ConditionalFlowMatching`, `ReverseKL`, `NegativeLogL` | objectives; each is a callable returning a loss |
|
|
57
|
+
| `train(objective, params, num_steps, learning_rate)` | Adam loop, returns the loss history |
|
|
58
|
+
| `ImportanceCorrection`, `coverage_curve`, `latent_pcn` | diagnostics and an exact latent-space MCMC sampler |
|
|
59
|
+
|
|
60
|
+
Every field takes `mode_scale=measure.scale`: the nonlinearity sees whitened coefficients and the
|
|
61
|
+
divergence is unchanged (similarity transform). Always pass it.
|
|
62
|
+
|
|
63
|
+
## 1. Learn a prior from samples (flow matching)
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
import torch
|
|
67
|
+
from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
|
|
68
|
+
from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
|
|
69
|
+
MatrixField, TimeBasisConditioner)
|
|
70
|
+
from FuncFlows.objectives import FlowMatching
|
|
71
|
+
from FuncFlows.utils.train import train
|
|
72
|
+
|
|
73
|
+
M = 64
|
|
74
|
+
basis = CosineBasis(M)
|
|
75
|
+
coeffs = ... # [N, M] training functions projected onto the basis
|
|
76
|
+
measure = GaussianReferenceMeasure(basis, variances=coeffs.var(0)) # diagonal Gaussian fit = the GP baseline
|
|
77
|
+
|
|
78
|
+
field = SumField(
|
|
79
|
+
LinearField(M, num_time_modes=4),
|
|
80
|
+
MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=measure.scale))
|
|
81
|
+
flow = ContinuousTransformation(measure, field, num_steps=16)
|
|
82
|
+
|
|
83
|
+
losses = train(FlowMatching(flow, coeffs, batch_size=256, weights=1 / measure.scale, coupling="optimal"),
|
|
84
|
+
flow.parameters(), num_steps=10000, learning_rate=2e-3)
|
|
85
|
+
|
|
86
|
+
draws = flow.transport(measure.sample(1000)) # [1000, M] new functions
|
|
87
|
+
points = torch.linspace(0, 1, 400).reshape(-1, 1)
|
|
88
|
+
values = draws @ basis.evaluate(points).T # [1000, 400] on a grid
|
|
89
|
+
log_q = flow.log_rn_at(draws) # exact log density w.r.t. the reference measure
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`weights=1/measure.scale` whitens the loss so high modes count; `coupling="optimal"` needs
|
|
93
|
+
scipy and only makes sense for an unconditional flow.
|
|
94
|
+
|
|
95
|
+
## 2. Posterior from a likelihood (no samples needed)
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from FuncFlows.objectives import ReverseKL
|
|
99
|
+
from FuncFlows.utils.gaussian_misfit import GaussianMisfit
|
|
100
|
+
from FuncFlows.diagnostics import ImportanceCorrection
|
|
101
|
+
|
|
102
|
+
prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0) # Matérn-like spectrum
|
|
103
|
+
phi_obs = basis.evaluate(obs_points) # [n_obs, M]
|
|
104
|
+
forward = lambda v: (v @ phi_obs.T) ** 2 # any differentiable map
|
|
105
|
+
potential = GaussianMisfit(forward, data, noise_std) # -log likelihood, [batch] -> [batch]
|
|
106
|
+
|
|
107
|
+
flow = ContinuousTransformation(prior, field, num_steps=16) # same field recipe as above
|
|
108
|
+
train(ReverseKL(flow, potential, num_samples=64, path_gradient=True), flow.parameters(), num_steps=3000)
|
|
109
|
+
|
|
110
|
+
draws = flow.transport(prior.sample(2000))
|
|
111
|
+
check = ImportanceCorrection(flow, draws, potential) # exact because the traces are exact
|
|
112
|
+
print(check.efficiency, check.log_evidence) # efficiency near 1 = posterior matched
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
For an exact sampler on top of a learned prior use `latent_pcn(flow, potential)`: pCN in the flow's
|
|
116
|
+
latent space, whose acceptance needs no Jacobian.
|
|
117
|
+
|
|
118
|
+
## 3. Amortised posterior (condition on data)
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from FuncFlows.transports.continuous import DataConditioner
|
|
122
|
+
from FuncFlows.objectives import ConditionalFlowMatching
|
|
123
|
+
|
|
124
|
+
C = ... # context dimension (e.g. whitened projected data)
|
|
125
|
+
field = SumField(
|
|
126
|
+
LinearField(M, C, num_time_modes=4, mode_scale=measure.scale), # data drift: the linear-Gaussian mean
|
|
127
|
+
MatrixField(DataConditioner(M, 256, C, num_time_modes=4), mode_scale=measure.scale))
|
|
128
|
+
flow = ContinuousTransformation(measure, field, num_steps=16)
|
|
129
|
+
|
|
130
|
+
def simulate(batch): # -> (target coeffs [batch, M], context [batch, C])
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
train(ConditionalFlowMatching(flow, simulate, measure.sample, batch_size=256, weights=1 / measure.scale),
|
|
134
|
+
flow.parameters(), num_steps=20000, learning_rate=2e-3)
|
|
135
|
+
|
|
136
|
+
posterior = flow.transport(measure.sample(1000), context.expand(1000, -1)) # one ODE solve per posterior
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Whiten the context by `measure.scale` when it is itself a projected function; otherwise the drift
|
|
140
|
+
weights for high modes have to reach 1/σ and never do. When a conjugate (linear-Gaussian)
|
|
141
|
+
posterior is available in closed form, use it as the base measure and let the flow learn the residual.
|
|
142
|
+
|
|
143
|
+
## Diagnostics
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from FuncFlows.diagnostics import coverage_curve, coverage_error
|
|
147
|
+
levels, coverage = coverage_curve(samples, truths, weights=1 / measure.scale) # TARP; samples [cases, draws, M]
|
|
148
|
+
coverage_error(levels, coverage) # signed max gap from the diagonal: negative = overconfident
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Layout
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
FuncFlows/
|
|
155
|
+
base_measures/ bases, GaussianReferenceMeasure
|
|
156
|
+
transports/ continuous/: fields, conditioners, ContinuousTransformation
|
|
157
|
+
(grid_fields.py: PointwiseField / OperatorField, spatial layers — experimental)
|
|
158
|
+
objectives/ flow_matching, reverse_kl, negative_logl
|
|
159
|
+
samplers/ latent_pcn
|
|
160
|
+
diagnostics/ importance, coverage
|
|
161
|
+
utils/ train, gaussian_misfit
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Full Example
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
import matplotlib
|
|
171
|
+
matplotlib.use("Agg")
|
|
172
|
+
import matplotlib.pyplot as plt
|
|
173
|
+
import torch
|
|
174
|
+
|
|
175
|
+
from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
|
|
176
|
+
from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
|
|
177
|
+
MatrixField, TimeBasisConditioner)
|
|
178
|
+
from FuncFlows.objectives import ReverseKL, FlowMatching
|
|
179
|
+
from FuncFlows.samplers import latent_pcn
|
|
180
|
+
from FuncFlows.utils.train import train
|
|
181
|
+
from FuncFlows.utils.gaussian_misfit import GaussianMisfit
|
|
182
|
+
from FuncFlows.diagnostics import ImportanceCorrection
|
|
183
|
+
|
|
184
|
+
torch.manual_seed(1)
|
|
185
|
+
M, NUM_OBS, NOISE, N = 32, 15, 0.05, 2000
|
|
186
|
+
basis = CosineBasis(M)
|
|
187
|
+
prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0)
|
|
188
|
+
x = torch.linspace(0, 1, 400, dtype=torch.float64)[:, None]
|
|
189
|
+
design = basis.evaluate(x)
|
|
190
|
+
|
|
191
|
+
# data || y = f(x)^2 + noise at NUM_OBS random points
|
|
192
|
+
truth = prior.sample(1)
|
|
193
|
+
obs_x = torch.rand(NUM_OBS, 1, dtype=torch.float64)
|
|
194
|
+
phi_obs = basis.evaluate(obs_x)
|
|
195
|
+
forward = lambda v: (v @ phi_obs.T) ** 2
|
|
196
|
+
data = forward(truth)[0] + NOISE * torch.randn(NUM_OBS, dtype=torch.float64)
|
|
197
|
+
misfit = GaussianMisfit(forward, data, NOISE) # -logl
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def make_flow():
|
|
201
|
+
field = SumField(LinearField(M, num_time_modes=4),
|
|
202
|
+
MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=prior.scale))
|
|
203
|
+
return ContinuousTransformation(prior, field, num_steps=16)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@torch.no_grad()
|
|
207
|
+
def pcn(potential, num_chains=64, num_steps=20000, beta=0.15, burn=5000, thin=20):
|
|
208
|
+
state, keep = prior.sample(num_chains), []
|
|
209
|
+
energy = potential(state)
|
|
210
|
+
for step in range(num_steps):
|
|
211
|
+
proposal = (1 - beta ** 2) ** 0.5 * state + beta * prior.sample(num_chains)
|
|
212
|
+
proposal_energy = potential(proposal)
|
|
213
|
+
accept = torch.log(torch.rand_like(energy)) < energy - proposal_energy
|
|
214
|
+
state = torch.where(accept[:, None], proposal, state)
|
|
215
|
+
energy = torch.where(accept, proposal_energy, energy)
|
|
216
|
+
if step >= burn and step % thin == 0:
|
|
217
|
+
keep.append(state.clone())
|
|
218
|
+
return torch.cat(keep)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def laplace_gp():
|
|
222
|
+
white = torch.zeros(M, dtype=torch.float64, requires_grad=True)
|
|
223
|
+
opt = torch.optim.LBFGS([white], max_iter=200, line_search_fn="strong_wolfe")
|
|
224
|
+
|
|
225
|
+
def closure():
|
|
226
|
+
opt.zero_grad()
|
|
227
|
+
loss = misfit(white * prior.scale) + 0.5 * (white ** 2).sum()
|
|
228
|
+
loss.backward()
|
|
229
|
+
return loss
|
|
230
|
+
|
|
231
|
+
opt.step(closure)
|
|
232
|
+
with torch.no_grad():
|
|
233
|
+
jac = 2 * (phi_obs @ (white * prior.scale))[:, None] * phi_obs * prior.scale
|
|
234
|
+
cov = torch.linalg.inv(torch.eye(M, dtype=torch.float64) + jac.T @ jac / NOISE ** 2)
|
|
235
|
+
return (white + torch.randn(N, M, dtype=torch.float64) @ torch.linalg.cholesky(cov).T) * prior.scale
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# run everything
|
|
239
|
+
results = {}
|
|
240
|
+
results["pCN reference"] = pcn(misfit)[torch.randperm(64 * 750)[:N]] # thin to N for the comparisons
|
|
241
|
+
|
|
242
|
+
results["GP (Laplace)"] = laplace_gp()
|
|
243
|
+
|
|
244
|
+
rkl = make_flow()
|
|
245
|
+
train(ReverseKL(rkl, misfit, num_samples=64, path_gradient=True), rkl.parameters(), num_steps=2500, learning_rate=3e-3)
|
|
246
|
+
with torch.no_grad():
|
|
247
|
+
results["reverse KL"] = rkl.transport(prior.sample(N))
|
|
248
|
+
|
|
249
|
+
fm = make_flow()
|
|
250
|
+
train(FlowMatching(fm, results["pCN reference"], batch_size=256), fm.parameters(), num_steps=4000, learning_rate=3e-3)
|
|
251
|
+
with torch.no_grad():
|
|
252
|
+
results["flow matching"] = fm.transport(prior.sample(N))
|
|
253
|
+
|
|
254
|
+
# pCN in the latent space of the posterior flow: target exp(-Phi(Tz)) prior(Tz)/q(Tz) mu0(dz), so add the
|
|
255
|
+
# flow's exact log-density to the potential. A good flow makes the target nearly flat -> big steps accepted.
|
|
256
|
+
corrected = lambda v: misfit(v) + fm.log_rn_at(v) # noqa: E731
|
|
257
|
+
draws, info = latent_pcn(fm, corrected, num_chains=128, num_steps=1500, beta=0.5, thin=10)
|
|
258
|
+
results["latent pCN"] = draws[torch.randperm(len(draws))[:N]]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# report
|
|
263
|
+
ref = results["pCN reference"] @ design.T
|
|
264
|
+
print(f"\n{'method':16s} {'+sign':>6s} {'E-dist':>8s} (ideal 0.50; distance to pCN)")
|
|
265
|
+
for name, v in results.items():
|
|
266
|
+
values = v @ design.T
|
|
267
|
+
sign = ((v @ truth[0]) > 0).double().mean().item()
|
|
268
|
+
dist = (2 * torch.cdist(values, ref).mean() - torch.cdist(values, values).mean() - torch.cdist(ref, ref).mean()) / 20
|
|
269
|
+
print(f"{name:16s} {sign:6.2f} {dist.item():8.4f}")
|
|
270
|
+
for name, flow in [("reverse KL", rkl), ("flow matching", fm)]:
|
|
271
|
+
with torch.no_grad():
|
|
272
|
+
c = ImportanceCorrection(flow, results[name], misfit)
|
|
273
|
+
print(f"{name}: importance efficiency {c.efficiency:.3f}, log evidence {c.log_evidence:.2f}")
|
|
274
|
+
print(f"latent pCN acceptance {info['acceptance']:.2f} at beta 0.5 (plain pCN would be ~0)")
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# figures
|
|
278
|
+
fig, axes = plt.subplots(1, len(results), figsize=(3.6 * len(results), 3.2), sharey=True)
|
|
279
|
+
tv = (truth @ design.T)[0]
|
|
280
|
+
for ax, (name, v) in zip(axes, results.items()):
|
|
281
|
+
ax.plot(x[:, 0], (v[:40] @ design.T).T, color="C0", alpha=0.15, lw=1)
|
|
282
|
+
ax.plot(x[:, 0], tv, "k", lw=1.5)
|
|
283
|
+
ax.plot(x[:, 0], -tv, "k--", lw=1.5)
|
|
284
|
+
ax.scatter(obs_x[:, 0], data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
|
|
285
|
+
ax.scatter(obs_x[:, 0], -data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
|
|
286
|
+
ax.set_title(name, fontsize=10)
|
|
287
|
+
fig.suptitle("y = f(x)^2 + noise: black = ±truth, red = ±sqrt(data), blue = posterior draws", fontsize=10)
|
|
288
|
+
fig.tight_layout()
|
|
289
|
+
fig.savefig("bimodal_posterior.png", dpi=130)
|
|
290
|
+
print("saved bimodal_posterior.png")
|
|
291
|
+
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "funcflows"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.3"
|
|
8
8
|
description = "Normalizing flows and flow matching on function spaces, with exact-trace vector fields"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -25,7 +25,7 @@ ot = ["scipy"] # optimal-transport coupling in flow matchi
|
|
|
25
25
|
test = ["pytest"]
|
|
26
26
|
|
|
27
27
|
[project.urls]
|
|
28
|
-
Homepage = "https://
|
|
28
|
+
Homepage = "https://pypi.org/project/funcflows/"
|
|
29
29
|
|
|
30
30
|
[tool.setuptools.packages.find]
|
|
31
31
|
include = ["FuncFlows*"]
|
funcflows-0.1.2/PKG-INFO
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: funcflows
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Summary: Normalizing flows and flow matching on function spaces, with exact-trace vector fields
|
|
5
|
-
Author: Liam Pinchbeck
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/YOUR_USER/FuncFlows
|
|
8
|
-
Classifier: Development Status :: 3 - Alpha
|
|
9
|
-
Classifier: Intended Audience :: Science/Research
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Topic :: Scientific/Engineering
|
|
13
|
-
Requires-Python: >=3.10
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
License-File: LICENSE
|
|
16
|
-
Requires-Dist: torch
|
|
17
|
-
Requires-Dist: torchvision
|
|
18
|
-
Requires-Dist: tqdm
|
|
19
|
-
Requires-Dist: scipy
|
|
20
|
-
Requires-Dist: matplotlib
|
|
21
|
-
Provides-Extra: ot
|
|
22
|
-
Requires-Dist: scipy; extra == "ot"
|
|
23
|
-
Provides-Extra: test
|
|
24
|
-
Requires-Dist: pytest; extra == "test"
|
|
25
|
-
Dynamic: license-file
|
|
26
|
-
|
|
27
|
-
# FuncFlows
|
|
28
|
-
|
|
29
|
-
Normalizing flows and flow matching on function spaces. Functions are represented by
|
|
30
|
-
coefficients on a Laplacian eigenbasis (cosine or Fourier), the reference measure is a Gaussian
|
|
31
|
-
on those coefficients, and transports are neural ODEs whose vector fields (`LinearField`,
|
|
32
|
-
`MatrixField`, `PointwiseField`) all have closed-form divergences, so densities relative to the
|
|
33
|
-
reference measure are exact.
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
pip install funcflows
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Objectives: flow matching (conditional / amortised), reverse KL, alpha-divergences, negative
|
|
40
|
-
log-likelihood. Samplers and diagnostics: latent-space pCN, importance correction, TARP coverage.
|
|
41
|
-
|
|
42
|
-
`torch` is a hard dependency and is large; install it first from pytorch.org if you need a
|
|
43
|
-
specific CUDA/MPS build.
|
funcflows-0.1.2/README.md
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# FuncFlows
|
|
2
|
-
|
|
3
|
-
Normalizing flows and flow matching on function spaces. Functions are represented by
|
|
4
|
-
coefficients on a Laplacian eigenbasis (cosine or Fourier), the reference measure is a Gaussian
|
|
5
|
-
on those coefficients, and transports are neural ODEs whose vector fields (`LinearField`,
|
|
6
|
-
`MatrixField`, `PointwiseField`) all have closed-form divergences, so densities relative to the
|
|
7
|
-
reference measure are exact.
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
pip install funcflows
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Objectives: flow matching (conditional / amortised), reverse KL, alpha-divergences, negative
|
|
14
|
-
log-likelihood. Samplers and diagnostics: latent-space pCN, importance correction, TARP coverage.
|
|
15
|
-
|
|
16
|
-
`torch` is a hard dependency and is large; install it first from pytorch.org if you need a
|
|
17
|
-
specific CUDA/MPS build.
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: funcflows
|
|
3
|
-
Version: 0.1.2
|
|
4
|
-
Summary: Normalizing flows and flow matching on function spaces, with exact-trace vector fields
|
|
5
|
-
Author: Liam Pinchbeck
|
|
6
|
-
License: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/YOUR_USER/FuncFlows
|
|
8
|
-
Classifier: Development Status :: 3 - Alpha
|
|
9
|
-
Classifier: Intended Audience :: Science/Research
|
|
10
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Topic :: Scientific/Engineering
|
|
13
|
-
Requires-Python: >=3.10
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
License-File: LICENSE
|
|
16
|
-
Requires-Dist: torch
|
|
17
|
-
Requires-Dist: torchvision
|
|
18
|
-
Requires-Dist: tqdm
|
|
19
|
-
Requires-Dist: scipy
|
|
20
|
-
Requires-Dist: matplotlib
|
|
21
|
-
Provides-Extra: ot
|
|
22
|
-
Requires-Dist: scipy; extra == "ot"
|
|
23
|
-
Provides-Extra: test
|
|
24
|
-
Requires-Dist: pytest; extra == "test"
|
|
25
|
-
Dynamic: license-file
|
|
26
|
-
|
|
27
|
-
# FuncFlows
|
|
28
|
-
|
|
29
|
-
Normalizing flows and flow matching on function spaces. Functions are represented by
|
|
30
|
-
coefficients on a Laplacian eigenbasis (cosine or Fourier), the reference measure is a Gaussian
|
|
31
|
-
on those coefficients, and transports are neural ODEs whose vector fields (`LinearField`,
|
|
32
|
-
`MatrixField`, `PointwiseField`) all have closed-form divergences, so densities relative to the
|
|
33
|
-
reference measure are exact.
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
pip install funcflows
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Objectives: flow matching (conditional / amortised), reverse KL, alpha-divergences, negative
|
|
40
|
-
log-likelihood. Samplers and diagnostics: latent-space pCN, importance correction, TARP coverage.
|
|
41
|
-
|
|
42
|
-
`torch` is a hard dependency and is large; install it first from pytorch.org if you need a
|
|
43
|
-
specific CUDA/MPS build.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|