Skip to content

Commit 070910c

Browse files
authored
feat: unify flow matching and score-based models (#1497)
* added neural_odes * move vector field estimator and extend * common methods for flow and sde estimators * rename neural_odes folder to ode_solvers * Update vector_field_estimator.py * update to work with vector field instances * Update score_estimator.py * Update flowmatching_estimator.py * add ConditionalVectorFieldEstimator to imports * Create vector_field_inference.py * NPSE is not subclass of VectorFieldInference * FMPE is not subclass of VectorFieldInference * Create vector_field_potential.py * Make score_fn_iid work with ConditionalVectorFieldEstimator * Update score_based_potential.py * Create vector_field_posterior.py * use new vector_field_estimator_based_potential * remove flow builders from test_correctness_of_batched_vs_seperate_sample_and_log_prob The test relies on sample and log_prob methods of the estimators. FlowMatchiningEstimator does not implement these methods. * import vector_field_estimator_based_potential * ruff format * remove uniform prior from test_c2st_fmpe_on_linearGaussian VectorFieldPosterior does not yet support norm_posterior * correct init of VectorFieldPosterior * remove FMPE from test_embedding_net_api the test assumes DirectPosterior and does not support new vector field implementation of FMPE * refactor * call score method to support flow matching * add vector field imports * refactor * ruff format * add docstrings for FlowMatchingEstimator * set track_gradients False by default * remove old score classes * better docstrings and refactor * pass kwargs to potential * minor improvements * refactor * optimize tests for vector field inference * ruff format * resolve pyright error * ruff format * fix bug in test_c2st_fmpe_on_linearGaussian * raise error on unexpected sample_with * bug fix - call score instead of forward * add update_params method * minor change * unified tests and refactor * rename PosteriorVectorFieldBasedPotential to VectorFieldBasedPotential * better error messages if score or marginals are not defined * rename FNPEScoreFunction to FactorizedNPEScoreFunction * shapes consistent with score estimators * skip some slow tests * update comments * fix merge bug * fix typo * add sigma_min to sde methods * handle devices * todos for PR 1501 * better docstrings * change condition_event_shape from 7 to 2 * move ConditionalVectorFieldEstimator to base * refactor ode solvers * add VectorFieldEstimatorBuilder protocol and refactor * use Literal for type annotation * minor refactor * fix docstring * add kwargs to constructor * minor refactoring * update docstring and refactor * import check_c2st from sbi.utils.metrics * better note * minor refactoring
1 parent 8a5c51d commit 070910c

27 files changed

Lines changed: 2442 additions & 1878 deletions

sbi/diagnostics/sbc.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from torch.distributions import Uniform
1111
from tqdm.auto import tqdm
1212

13-
from sbi.inference import DirectPosterior, ScorePosterior
13+
from sbi.inference import DirectPosterior, VectorFieldPosterior
1414
from sbi.inference.posteriors.base_posterior import NeuralPosterior
1515
from sbi.inference.posteriors.vi_posterior import VIPosterior
1616
from sbi.utils.diagnostics_utils import (
@@ -186,7 +186,9 @@ def get_nltp(thetas: Tensor, xs: Tensor, posterior: NeuralPosterior) -> Tensor:
186186
nltp: negative log probs of true parameters under approximate posteriors.
187187
"""
188188
nltp = torch.zeros(thetas.shape[0])
189-
unnormalized_log_prob = not isinstance(posterior, (DirectPosterior, ScorePosterior))
189+
unnormalized_log_prob = not isinstance(
190+
posterior, (DirectPosterior, VectorFieldPosterior)
191+
)
190192

191193
for idx, (tho, xo) in enumerate(zip(thetas, xs, strict=False)):
192194
# Log prob of true params under posterior.

sbi/inference/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,14 @@
3838
ImportanceSamplingPosterior,
3939
MCMCPosterior,
4040
RejectionPosterior,
41-
ScorePosterior,
4241
VIPosterior,
42+
VectorFieldPosterior,
4343
)
4444
from sbi.inference.potentials import (
4545
likelihood_estimator_based_potential,
4646
mixed_likelihood_estimator_based_potential,
4747
posterior_estimator_based_potential,
4848
ratio_estimator_based_potential,
49+
vector_field_estimator_based_potential,
4950
)
5051
from sbi.utils.simulation_utils import simulate_for_sbi

sbi/inference/posteriors/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@
33
from sbi.inference.posteriors.importance_posterior import ImportanceSamplingPosterior
44
from sbi.inference.posteriors.mcmc_posterior import MCMCPosterior
55
from sbi.inference.posteriors.rejection_posterior import RejectionPosterior
6-
from sbi.inference.posteriors.score_posterior import ScorePosterior
6+
from sbi.inference.posteriors.vector_field_posterior import VectorFieldPosterior
77
from sbi.inference.posteriors.vi_posterior import VIPosterior

sbi/inference/posteriors/score_posterior.py renamed to sbi/inference/posteriors/vector_field_posterior.py

Lines changed: 85 additions & 66 deletions
Large diffs are not rendered by default.

sbi/inference/potentials/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@
88
from sbi.inference.potentials.ratio_based_potential import (
99
ratio_estimator_based_potential,
1010
)
11+
from sbi.inference.potentials.vector_field_potential import (
12+
vector_field_estimator_based_potential,
13+
)

sbi/inference/potentials/score_fn_iid.py

Lines changed: 65 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from torch import Tensor
88
from torch.distributions import Distribution
99

10-
from sbi.neural_nets.estimators.score_estimator import ConditionalScoreEstimator
10+
from sbi.neural_nets.estimators import ConditionalVectorFieldEstimator
1111
from sbi.utils.score_utils import (
1212
add_diag_or_dense,
1313
denoise,
@@ -58,7 +58,7 @@ def decorator(cls: Type["IIDScoreFunction"]) -> Type["IIDScoreFunction"]:
5858
class IIDScoreFunction(ABC):
5959
def __init__(
6060
self,
61-
score_estimator: "ConditionalScoreEstimator",
61+
vector_field_estimator: ConditionalVectorFieldEstimator,
6262
prior: Distribution,
6363
device: str = "cpu",
6464
) -> None:
@@ -76,12 +76,34 @@ def __init__(
7676
this requires some adjustments.
7777
7878
Args:
79-
score_estimator: The neural network modeling the score.
79+
vector_field_estimator: The neural network modeling the score.
8080
prior: The prior distribution.
8181
device: The device on which to evaluate the potential. Defaults to "cpu".
8282
"""
8383

84-
self.score_estimator = score_estimator.to(device).eval()
84+
if not vector_field_estimator.SCORE_DEFINED:
85+
raise ValueError(
86+
"Score is not defined for this vector field estimator. "
87+
"You are probably using a vector field estimator that does not "
88+
"implement the score function, e.g. an optimal transport-based "
89+
"flow matching. IID methods require the score function to be defined "
90+
"so they are not applicable to this vector field estimator. "
91+
"If you have implemented a custom vector field "
92+
"estimator with score defined, set SCORE_DEFINED to True."
93+
)
94+
if not vector_field_estimator.MARGINALS_DEFINED:
95+
raise ValueError(
96+
"Marginals are not defined for this vector field estimator. "
97+
"You are probably using a vector field estimator that does not "
98+
"implement the marginals mean_t_fn and std_fn, e.g. an "
99+
"optimal transport-based flow matching. IID methods require the "
100+
"marginals to be defined so they are not applicable to this "
101+
"vector field estimator. "
102+
"If you have implemented a custom vector field "
103+
"estimator with marginals defined, set MARGINALS_DEFINED to True."
104+
)
105+
106+
self.vector_field_estimator = vector_field_estimator.to(device).eval()
85107
self.prior = prior
86108

87109
@abstractmethod
@@ -106,17 +128,17 @@ def __call__(
106128

107129

108130
@register_iid_method("fnpe")
109-
class FNPEScoreFunction(IIDScoreFunction):
131+
class FactorizedNPEScoreFunction(IIDScoreFunction):
110132
def __init__(
111133
self,
112-
score_estimator: "ConditionalScoreEstimator",
134+
vector_field_estimator: ConditionalVectorFieldEstimator,
113135
prior: Distribution,
114136
device: str = "cpu",
115137
prior_score_weight: Optional[Callable[[Tensor], Tensor]] = None,
116138
) -> None:
117139
r"""
118-
The FNPEScoreFunction implments the "Factorized Neural Posterior Estimation"
119-
method for score-based models [1].
140+
The FactorizedNPEScoreFunction implments the
141+
"Factorized Neural Posterior Estimation" method for score-based models [1].
120142
121143
This method does not apply the necessary corrections for the score function, but
122144
instead uses a simple weighting of the prior score. This is generally applicable
@@ -131,15 +153,15 @@ def __init__(
131153
(https://arxiv.org/abs/2209.14249)
132154
133155
Args:
134-
score_estimator: The neural network modeling the score.
156+
vector_field_estimator: The neural network modeling the score.
135157
prior: The prior distribution.
136158
device: The device on which to evaluate the potential. Defaults to "cpu".
137159
prior_score_weight: A function to weight the prior score. Defaults to the
138160
linear interpolation between zero (at t=0) and one (at t=t_max).
139161
"""
140-
super().__init__(score_estimator, prior, device)
162+
super().__init__(vector_field_estimator, prior, device)
141163
if prior_score_weight is None:
142-
t_max = score_estimator.t_max
164+
t_max = vector_field_estimator.t_max
143165

144166
def prior_score_weight_fn(t):
145167
return (t_max - t) / t_max
@@ -168,13 +190,13 @@ def __call__(
168190
assert conditions.ndim == 2, "Conditions must have shape [iid,...]"
169191

170192
if time is None:
171-
time = torch.tensor([self.score_estimator.t_min])
193+
time = torch.tensor([self.vector_field_estimator.t_min])
172194

173195
N = conditions.shape[0]
174196

175197
# Compute the per-sample score
176198
inputs = ensure_theta_batched(inputs)
177-
base_score = self.score_estimator(inputs, conditions, time)
199+
base_score = self.vector_field_estimator.score(inputs, conditions, time)
178200

179201
# Compute the prior score
180202
prior_score = self.prior_score_weight_fn(time) * self.prior_score_fn(inputs)
@@ -214,7 +236,7 @@ def prior_score_fn(self, theta: Tensor) -> Tensor:
214236
class BaseGaussCorrectedScoreFunction(IIDScoreFunction):
215237
def __init__(
216238
self,
217-
score_estimator: "ConditionalScoreEstimator",
239+
vector_field_estimator: ConditionalVectorFieldEstimator,
218240
prior: Distribution,
219241
ensure_lam_psd: bool = True,
220242
lam_psd_nugget: float = 0.01,
@@ -244,13 +266,13 @@ def __init__(
244266
(https://arxiv.org/abs/2411.02728)
245267
246268
Args:
247-
score_estimator: The neural network modelling the score.
269+
vector_field_estimator: The neural network modelling the score.
248270
prior: The prior distribution.
249271
ensure_lam_psd: Whether to ensure the precision matrix is positive definite.
250272
lam_psd_nugget: The nugget value to ensure positive definiteness.
251273
device: The device on which to evaluate the potential. Defaults to "cpu".
252274
"""
253-
super().__init__(score_estimator, prior, device)
275+
super().__init__(vector_field_estimator, prior, device)
254276
self.ensure_lam_psd = ensure_lam_psd
255277
self.lam_psd_nugget = lam_psd_nugget
256278

@@ -289,8 +311,8 @@ def marginal_denoising_posterior_precision_est_fn(
289311
precisions_posteriors = self.posterior_precision_est_fn(conditions)
290312

291313
# Denoising posterior via Bayes rule
292-
m = self.score_estimator.mean_t_fn(time)
293-
std = self.score_estimator.std_fn(time)
314+
m = self.vector_field_estimator.mean_t_fn(time)
315+
std = self.vector_field_estimator.std_fn(time)
294316

295317
if precisions_posteriors.ndim == 4:
296318
Ident = torch.eye(precisions_posteriors.shape[-1])
@@ -315,8 +337,8 @@ def marginal_prior_score_fn(self, time: Tensor, inputs: Tensor) -> Tensor:
315337
try:
316338
with torch.enable_grad():
317339
inputs = inputs.clone().detach().requires_grad_(True)
318-
m = self.score_estimator.mean_t_fn(time)
319-
std = self.score_estimator.std_fn(time)
340+
m = self.vector_field_estimator.mean_t_fn(time)
341+
std = self.vector_field_estimator.std_fn(time)
320342
p = marginalize(self.prior, m, std)
321343
log_p = p.log_prob(inputs)
322344
prior_score = torch.autograd.grad(
@@ -342,8 +364,8 @@ def marginal_denoising_prior_precision_fn(
342364
Returns:
343365
Marginal denoising prior precision.
344366
"""
345-
m = self.score_estimator.mean_t_fn(time)
346-
std = self.score_estimator.std_fn(time)
367+
m = self.vector_field_estimator.mean_t_fn(time)
368+
std = self.vector_field_estimator.std_fn(time)
347369

348370
p_denoise = denoise(self.prior, m, std, inputs)
349371

@@ -388,9 +410,11 @@ def __call__(
388410
N, *_ = conditions.shape
389411

390412
if time is None:
391-
time = torch.tensor([self.score_estimator.t_min])
413+
time = torch.tensor([self.vector_field_estimator.t_min])
392414

393-
base_score = self.score_estimator(inputs, conditions, time, **kwargs)
415+
base_score = self.vector_field_estimator.score(
416+
inputs, conditions, time, **kwargs
417+
)
394418
prior_score = self.marginal_prior_score_fn(time, inputs)
395419

396420
# Marginal prior precision
@@ -438,7 +462,7 @@ def __call__(
438462
class GaussCorrectedScoreFn(BaseGaussCorrectedScoreFunction):
439463
def __init__(
440464
self,
441-
score_estimator: "ConditionalScoreEstimator",
465+
vector_field_estimator: ConditionalVectorFieldEstimator,
442466
prior: Distribution,
443467
posterior_precision: Optional[Tensor] = None,
444468
scale_from_prior_precision: float = 2.0,
@@ -453,7 +477,7 @@ def __init__(
453477
precision. This method simply scales the prior precision by a factor.
454478
455479
Args:
456-
score_estimator: The neural network modeling the score.
480+
vector_field_estimator: The neural network modeling the score.
457481
prior: The prior distribution.
458482
posterior_precision: Optional preset posterior precision.
459483
scale_from_prior_precision: If not provided it simply increases the prior
@@ -463,7 +487,7 @@ def __init__(
463487
device: The device on which to evaluate the potential. Defaults to "cpu".
464488
"""
465489
super().__init__(
466-
score_estimator, prior, enable_lam_psd, lam_psd_nugget, device=device
490+
vector_field_estimator, prior, enable_lam_psd, lam_psd_nugget, device=device
467491
)
468492

469493
if posterior_precision is None:
@@ -511,7 +535,7 @@ def estimate_prior_precision(
511535
except Exception:
512536
d = prior.event_shape[0]
513537
num_samples = int(math.sqrt(d) * 1000)
514-
prior_samples = prior.sample((num_samples,))
538+
prior_samples = prior.sample(torch.Size((num_samples,)))
515539
prior_precision_estimate = 1 / torch.var(prior_samples, dim=0)
516540
posterior_precision = scale_from_prior_precision * prior_precision_estimate
517541
return posterior_precision
@@ -521,7 +545,7 @@ def estimate_prior_precision(
521545
class AutoGaussCorrectedScoreFn(BaseGaussCorrectedScoreFunction):
522546
def __init__(
523547
self,
524-
score_estimator: "ConditionalScoreEstimator",
548+
vector_field_estimator: ConditionalVectorFieldEstimator,
525549
prior: Distribution,
526550
enable_lam_psd: bool = True,
527551
lam_psd_nugget: float = 0.01,
@@ -533,11 +557,11 @@ def __init__(
533557
r"""
534558
This method extends the by estimating the posterior precision using
535559
approximate posterior samples obtained from a diffusion model (using the
536-
score_estimator) [1]. This method has a slight initialization overhead, but
537-
generally provides more accurate results than simple heuristics.
560+
vector_field_estimator) [1]. This method has a slight initialization overhead,
561+
but generally provides more accurate results than simple heuristics.
538562
539563
Args:
540-
score_estimator: The neural network modeling the score.
564+
vector_field_estimator: The neural network modeling the score.
541565
prior: The prior distribution.
542566
enable_lam_psd: Whether to ensure the precision matrix is positive definite.
543567
lam_psd_nugget: The nugget value to ensure positive definiteness.
@@ -548,7 +572,7 @@ def __init__(
548572
device: The device on which to evaluate the potential. Defaults to "cpu".
549573
"""
550574
super().__init__(
551-
score_estimator, prior, enable_lam_psd, lam_psd_nugget, device=device
575+
vector_field_estimator, prior, enable_lam_psd, lam_psd_nugget, device=device
552576
)
553577
self.precision_est_only_diag = precision_est_only_diag
554578
self.precision_est_budget = precision_est_budget
@@ -565,7 +589,7 @@ def posterior_precision_est_fn(self, conditions: Tensor) -> Tensor:
565589
Estimated posterior precision.
566590
"""
567591
return self.estimate_posterior_precision(
568-
self.score_estimator,
592+
self.vector_field_estimator,
569593
self.prior,
570594
conditions,
571595
precision_est_only_diag=self.precision_est_only_diag,
@@ -577,7 +601,7 @@ def posterior_precision_est_fn(self, conditions: Tensor) -> Tensor:
577601
@functools.lru_cache()
578602
def estimate_posterior_precision(
579603
cls,
580-
score_estimator: "ConditionalScoreEstimator",
604+
vector_field_estimator: ConditionalVectorFieldEstimator,
581605
prior: Distribution,
582606
conditions: Tensor,
583607
precision_est_only_diag: bool = False,
@@ -589,7 +613,7 @@ def estimate_posterior_precision(
589613
for each observation, then empirically estimating the precision matrix.
590614
591615
Args:
592-
score_estimator: The score estimator.
616+
vector_field_estimator: The score estimator.
593617
prior: The prior distribution.
594618
conditions: Observed data.
595619
precision_est_only_diag: Whether to estimate only the diagonal of the
@@ -606,9 +630,9 @@ def estimate_posterior_precision(
606630
# the results efficiently.
607631

608632
# NOTE: To avoid circular imports :(
609-
from sbi.inference.posteriors.score_posterior import ScorePosterior
633+
from sbi.inference.posteriors.vector_field_posterior import VectorFieldPosterior
610634

611-
posterior = ScorePosterior(score_estimator, prior)
635+
posterior = VectorFieldPosterior(vector_field_estimator, prior)
612636

613637
if precision_est_budget is None:
614638
if precision_est_only_diag:
@@ -674,16 +698,16 @@ def marginal_denoising_posterior_precision_est_fn(
674698
with torch.enable_grad():
675699
# NOTE: torch.func can be realtively unstable...
676700
jac_fn = torch.func.jacrev( # type: ignore
677-
lambda x: self.score_estimator(x, conditions, time)
701+
lambda x: self.vector_field_estimator.score(x, conditions, time)
678702
)
679703
jac_fn = torch.func.vmap(torch.func.vmap(jac_fn)) # type: ignore
680704
jac = jac_fn(inputs).squeeze(1)
681705

682706
# Must be symmetrical
683707
jac = 0.5 * (jac + jac.transpose(-1, -2))
684708

685-
m = self.score_estimator.mean_t_fn(time)
686-
std = self.score_estimator.std_fn(time)
709+
m = self.vector_field_estimator.mean_t_fn(time)
710+
std = self.vector_field_estimator.std_fn(time)
687711
cov0 = std**2 * jac + torch.eye(d)[None, None, :, :]
688712

689713
denoising_posterior_precision = m**2 / std**2 + torch.inverse(cov0)

0 commit comments

Comments
 (0)