def get_proposal( task: Task, samples: torch.Tensor, prior_weight: float = 0.01, bounded: bool = True, density_estimator: str = "flow", flow_model: str = "nsf", **kwargs: Any, ) -> torch.Tensor: """Gets proposal distribution by performing density estimation on `samples` If `prior_weight` > 0., the proposal is defensive, i.e., the prior is mixed in Args: task: Task instance samples: Samples to fit prior_weight: Prior weight bounded: If True, will automatically transform proposal density to bounded space density_estimator: Density estimator flow_model: Flow to use if `density_estimator` is `flow` kwargs: Passed on to `get_flow` or `get_kde` Returns: Proposal distribution """ tic = time.time() log = sbibm.get_logger(__name__) log.info("Get proposal distribution called") prior_dist = task.get_prior_dist() transform = task._get_transforms( automatic_transforms_enabled=bounded)["parameters"] if density_estimator == "flow": density_estimator_ = get_flow(model=flow_model, dim_distribution=task.dim_parameters, **kwargs) density_estimator_ = train_flow(density_estimator_, samples, transform=transform) elif density_estimator == "kde": density_estimator_ = get_kde(X=samples, transform=transform, **kwargs) else: raise NotImplementedError proposal_dist = DenfensiveProposal( dim=task.dim_parameters, proposal=density_estimator_, prior=prior_dist, prior_weight=prior_weight, ) log.info(f"Proposal distribution is set up, took {time.time()-tic:.3f}sec") return proposal_dist
def ksd( task: Task, num_observation: int, samples: torch.Tensor, sig2: Optional[float] = None, log: bool = True, ) -> torch.Tensor: """Gets `log_prob_grad_fn` from task and runs KSD Args: task: Task num_observation: Observation samples: Samples sig2: Length scale log: Whether to log test result Returns: The test result is returned """ try: device = get_default_device() site_name = "parameters" log_prob_grad_fn = task._get_log_prob_grad_fn( num_observation=num_observation, implementation="pyro", posterior=True, jit_compile=False, automatic_transform_enabled=True, ) samples_transformed = task._get_transforms(automatic_transform_enabled=True)[ site_name ](samples) def log_prob_grad_numpy(parameters: np.ndarray) -> np.ndarray: lpg = log_prob_grad_fn( torch.from_numpy(parameters.astype(np.float32)).to(device) ) return lpg.detach().cpu().numpy() test_statistic = ksd_gaussian_kernel( log_prob_grad_numpy, samples_transformed, sig2=sig2 ) if log: return math.log(test_statistic) else: return test_statistic except: return torch.tensor(float("nan"))
def run( task: Task, num_samples: int, num_simulations: int, num_observation: Optional[int] = None, observation: Optional[torch.Tensor] = None, num_rounds: int = 10, neural_net: str = "nsf", hidden_features: int = 50, simulation_batch_size: int = 1000, training_batch_size: int = 10000, num_atoms: int = 10, automatic_transforms_enabled: bool = False, z_score_x: bool = True, z_score_theta: bool = True, ) -> Tuple[torch.Tensor, int, Optional[torch.Tensor]]: """Runs (S)NPE from `sbi` Args: task: Task instance num_samples: Number of samples to generate from posterior num_simulations: Simulation budget num_observation: Observation number to load, alternative to `observation` observation: Observation, alternative to `num_observation` num_rounds: Number of rounds neural_net: Neural network to use, one of maf / mdn / made / nsf hidden_features: Number of hidden features in network simulation_batch_size: Batch size for simulator training_batch_size: Batch size for training network num_atoms: Number of atoms, -1 means same as `training_batch_size` automatic_transforms_enabled: Whether to enable automatic transforms z_score_x: Whether to z-score x z_score_theta: Whether to z-score theta Returns: Samples from posterior, number of simulator calls, log probability of true params if computable """ assert not (num_observation is None and observation is None) assert not (num_observation is not None and observation is not None) log = logging.getLogger(__name__) if num_rounds == 1: log.info(f"Running NPE") num_simulations_per_round = num_simulations else: log.info(f"Running SNPE") num_simulations_per_round = math.floor(num_simulations / num_rounds) if simulation_batch_size > num_simulations_per_round: simulation_batch_size = num_simulations_per_round log.warn("Reduced simulation_batch_size to num_simulation_per_round") if training_batch_size > num_simulations_per_round: training_batch_size = num_simulations_per_round log.warn("Reduced training_batch_size to num_simulation_per_round") prior = task.get_prior_dist() if observation is None: observation = task.get_observation(num_observation) simulator = task.get_simulator(max_calls=num_simulations) transforms = task._get_transforms( automatic_transforms_enabled)["parameters"] if automatic_transforms_enabled: prior = wrap_prior_dist(prior, transforms) simulator = wrap_simulator_fn(simulator, transforms) density_estimator_fun = posterior_nn( model=neural_net.lower(), hidden_features=hidden_features, z_score_x=z_score_x, z_score_theta=z_score_theta, ) inference_method = inference.SNPE_C( prior, density_estimator=density_estimator_fun) posteriors = [] proposal = prior for _ in range(num_rounds): theta, x = inference.simulate_for_sbi( simulator, proposal, num_simulations=num_simulations_per_round, simulation_batch_size=simulation_batch_size, ) density_estimator = inference_method.append_simulations( theta, x, proposal=proposal).train( num_atoms=num_atoms, training_batch_size=training_batch_size, retrain_from_scratch_each_round=False, discard_prior_samples=False, use_combined_loss=False, show_train_summary=True, ) posterior = inference_method.build_posterior(density_estimator, sample_with_mcmc=False) proposal = posterior.set_default_x(observation) posteriors.append(posterior) posterior = wrap_posterior(posteriors[-1], transforms) assert simulator.num_simulations == num_simulations samples = posterior.sample((num_samples, )).detach() if num_observation is not None: true_parameters = task.get_true_parameters( num_observation=num_observation) log_prob_true_parameters = posterior.log_prob(true_parameters) return samples, simulator.num_simulations, log_prob_true_parameters else: return samples, simulator.num_simulations, None
def run( task: Task, num_samples: int, num_observation: Optional[int] = None, observation: Optional[torch.Tensor] = None, num_chains: int = 10, num_warmup: int = 10000, kernel: str = "slice", kernel_parameters: Optional[Dict[str, Any]] = None, thinning: int = 1, diagnostics: bool = True, available_cpu: int = 1, mp_context: str = "fork", jit_compile: bool = False, automatic_transforms_enabled: bool = True, initial_params: Optional[torch.Tensor] = None, **kwargs: Any, ) -> torch.Tensor: """Runs MCMC using Pyro on potential function Produces `num_samples` while accounting for warmup (burn-in) and thinning. Note that the actual number of simulations is not controlled for with MCMC since algorithms are only used as a reference method in the benchmark. MCMC is run on the potential function, which returns the unnormalized negative log posterior probability. Note that this requires a tractable likelihood. Pyro is used to automatically construct the potential function. Args: task: Task instance num_samples: Number of samples to generate from posterior num_observation: Observation number to load, alternative to `observation` observation: Observation, alternative to `num_observation` num_chains: Number of chains num_warmup: Warmup steps, during which parameters of the sampler are adapted. Warmup samples are not returned by the algorithm. kernel: HMC, NUTS, or Slice kernel_parameters: Parameters passed to kernel thinning: Amount of thinning to apply, in order to avoid drawing correlated samples from the chain diagnostics: Flag for diagnostics available_cpu: Number of CPUs used to parallelize chains mp_context: multiprocessing context, only fork might work jit_compile: Just-in-time (JIT) compilation, can yield significant speed ups automatic_transforms_enabled: Whether or not to use automatic transforms initial_params: Parameters to initialize at Returns: Samples from posterior """ assert not (num_observation is None and observation is None) assert not (num_observation is not None and observation is not None) tic = time.time() log = sbibm.get_logger(__name__) hook_fn = None if diagnostics: log.info(f"MCMC sampling for observation {num_observation}") tb_writer, tb_close = tb_make_writer( logger=log, basepath= f"tensorboard/pyro_{kernel.lower()}/observation_{num_observation}", ) hook_fn = tb_make_hook_fn(tb_writer) if "num_simulations" in kwargs: warnings.warn( "`num_simulations` was passed as a keyword but will be ignored, see docstring for more info." ) # Prepare model and transforms conditioned_model = task._get_pyro_model(num_observation=num_observation, observation=observation) transforms = task._get_transforms( num_observation=num_observation, observation=observation, automatic_transforms_enabled=automatic_transforms_enabled, ) kernel_parameters = kernel_parameters if kernel_parameters is not None else {} kernel_parameters["jit_compile"] = jit_compile kernel_parameters["transforms"] = transforms log.info("Using kernel: {name}({parameters})".format( name=kernel, parameters=",".join([f"{k}={v}" for k, v in kernel_parameters.items()]), )) if kernel.lower() == "nuts": mcmc_kernel = NUTS(model=conditioned_model, **kernel_parameters) elif kernel.lower() == "hmc": mcmc_kernel = HMC(model=conditioned_model, **kernel_parameters) elif kernel.lower() == "slice": mcmc_kernel = Slice(model=conditioned_model, **kernel_parameters) else: raise NotImplementedError if initial_params is not None: site_name = "parameters" initial_params = {site_name: transforms[site_name](initial_params)} else: initial_params = None mcmc_parameters = { "num_chains": num_chains, "num_samples": thinning * num_samples, "warmup_steps": num_warmup, "available_cpu": available_cpu, "initial_params": initial_params, } log.info("Calling MCMC with: MCMC({name}_kernel, {parameters})".format( name=kernel, parameters=",".join([f"{k}={v}" for k, v in mcmc_parameters.items()]), )) mcmc = MCMC(mcmc_kernel, hook_fn=hook_fn, **mcmc_parameters) mcmc.run() toc = time.time() log.info(f"Finished MCMC after {toc-tic:.3f} seconds") log.info(f"Automatic transforms {mcmc.transforms}") log.info(f"Apply thinning of {thinning}") mcmc._samples = { "parameters": mcmc._samples["parameters"][:, ::thinning, :] } num_samples_available = (mcmc._samples["parameters"].shape[0] * mcmc._samples["parameters"].shape[1]) if num_samples_available < num_samples: warnings.warn("Some samples will be included multiple times") samples = mcmc.get_samples( num_samples=num_samples, group_by_chain=False)["parameters"].squeeze() else: samples = mcmc.get_samples( group_by_chain=False)["parameters"].squeeze() idx = torch.randperm(samples.shape[0])[:num_samples] samples = samples[idx, :] assert samples.shape[0] == num_samples if diagnostics: mcmc.summary() tb_ess(tb_writer, mcmc) tb_r_hat(tb_writer, mcmc) tb_marginals(tb_writer, mcmc) tb_acf(tb_writer, mcmc) tb_posteriors(tb_writer, mcmc) tb_plot_posterior(tb_writer, samples, tag="posterior/final") tb_close() return samples
def run( task: Task, num_samples: int, num_simulations: int, num_observation: Optional[int] = None, observation: Optional[torch.Tensor] = None, num_rounds: int = 10, neural_net: str = "resnet", hidden_features: int = 50, simulation_batch_size: int = 1000, training_batch_size: int = 10000, num_atoms: int = 10, automatic_transforms_enabled: bool = True, mcmc_method: str = "slice_np_vectorized", mcmc_parameters: Dict[str, Any] = { "num_chains": 100, "thin": 10, "warmup_steps": 100, "init_strategy": "sir", "sir_batch_size": 1000, "sir_num_batches": 100, }, z_score_x: bool = True, z_score_theta: bool = True, variant: str = "B", ) -> Tuple[torch.Tensor, int, Optional[torch.Tensor]]: """Runs (S)NRE from `sbi` Args: task: Task instance num_samples: Number of samples to generate from posterior num_observation: Observation number to load, alternative to `observation` observation: Observation, alternative to `num_observation` num_simulations: Simulation budget num_rounds: Number of rounds neural_net: Neural network to use, one of linear / mlp / resnet hidden_features: Number of hidden features in network simulation_batch_size: Batch size for simulator training_batch_size: Batch size for training network num_atoms: Number of atoms, -1 means same as `training_batch_size` automatic_transforms_enabled: Whether to enable automatic transforms mcmc_method: MCMC method mcmc_parameters: MCMC parameters z_score_x: Whether to z-score x z_score_theta: Whether to z-score theta variant: Can be used to switch between SNRE-A (AALR) and -B (SRE) Returns: Samples from posterior, number of simulator calls, log probability of true params if computable """ assert not (num_observation is None and observation is None) assert not (num_observation is not None and observation is not None) log = logging.getLogger(__name__) if num_rounds == 1: log.info(f"Running NRE") num_simulations_per_round = num_simulations else: log.info(f"Running SNRE") num_simulations_per_round = math.floor(num_simulations / num_rounds) if simulation_batch_size > num_simulations_per_round: simulation_batch_size = num_simulations_per_round log.warn("Reduced simulation_batch_size to num_simulation_per_round") if training_batch_size > num_simulations_per_round: training_batch_size = num_simulations_per_round log.warn("Reduced training_batch_size to num_simulation_per_round") prior = task.get_prior_dist() if observation is None: observation = task.get_observation(num_observation) simulator = task.get_simulator(max_calls=num_simulations) transforms = task._get_transforms( automatic_transforms_enabled)["parameters"] if automatic_transforms_enabled: prior = wrap_prior_dist(prior, transforms) simulator = wrap_simulator_fn(simulator, transforms) classifier = classifier_nn( model=neural_net.lower(), hidden_features=hidden_features, z_score_x=z_score_x, z_score_theta=z_score_theta, ) if variant == "A": inference_class = inference.SNRE_A inference_method_kwargs = {} elif variant == "B": inference_class = inference.SNRE_B inference_method_kwargs = {"num_atoms": num_atoms} else: raise NotImplementedError inference_method = inference_class(classifier=classifier, prior=prior) posteriors = [] proposal = prior mcmc_parameters["warmup_steps"] = 25 for r in range(num_rounds): theta, x = inference.simulate_for_sbi( simulator, proposal, num_simulations=num_simulations_per_round, simulation_batch_size=simulation_batch_size, ) density_estimator = inference_method.append_simulations( theta, x, from_round=r).train( training_batch_size=training_batch_size, retrain_from_scratch_each_round=False, discard_prior_samples=False, show_train_summary=True, **inference_method_kwargs, ) if r > 1: mcmc_parameters["init_strategy"] = "latest_sample" posterior = inference_method.build_posterior( density_estimator, mcmc_method=mcmc_method, mcmc_parameters=mcmc_parameters) # Copy hyperparameters, e.g., mcmc_init_samples for "latest_sample" strategy. if r > 0: posterior.copy_hyperparameters_from(posteriors[-1]) proposal = posterior.set_default_x(observation) posteriors.append(posterior) posterior = wrap_posterior(posteriors[-1], transforms) assert simulator.num_simulations == num_simulations samples = posterior.sample((num_samples, )).detach() return samples, simulator.num_simulations, None
def run( task: Task, num_samples: int, num_simulations: int, num_observation: Optional[int] = None, observation: Optional[torch.Tensor] = None, population_size: Optional[int] = None, distance: Optional[str] = "l2", initial_round_factor: int = 5, batch_size: int = 1000, epsilon_decay: Optional[float] = 0.5, kernel: Optional[str] = "gaussian", kernel_variance_scale: Optional[float] = 0.5, population_strategy: Optional[str] = "constant", use_last_pop_samples: bool = False, num_workers: int = 1, sass: bool = False, sass_sample_weights: bool = False, sass_feature_expansion_degree: int = 1, sass_fraction: float = 0.5, lra: bool = False, lra_sample_weights: bool = True, kde_bandwidth: Optional[str] = None, kde_sample_weights: bool = False, ) -> Tuple[torch.Tensor, int, Optional[torch.Tensor]]: """ABC-SMC using pyabc toolbox Args: task: Task instance num_samples: Number of samples to generate from posterior num_simulations: Simulation budget num_observation: Observation number to load, alternative to `observation` observation: Observation, alternative to `num_observation` population_size: If None, uses heuristic: 1000 if `num_simulations` is greater than 10k, else 100 distance: Distance function, options = {l1, l2, mse} epsilon_decay: Decay for epsilon, quantile based. kernel: Kernel distribution used to perturb the particles. kernel_variance_scale: Scaling factor for kernel variance. sass: If True, summary statistics are learned as in Fearnhead & Prangle 2012. sass_sample_weights: Whether to weigh SASS samples sass_feature_expansion_degree: Degree of polynomial expansion of the summary statistics. sass_fraction: Fraction of simulation budget to use for sass. lra: If True, posterior samples are adjusted with linear regression as in Beaumont et al. 2002. lra_sample_weights: Whether to weigh LRA samples kde_bandwidth: If not None, will resample using KDE when necessary, set e.g. to "cv" for cross-validated bandwidth selection kde_sample_weights: Whether to weigh KDE samples Returns: Samples from posterior, number of simulator calls, log probability of true params if computable """ assert not (num_observation is None and observation is None) assert not (num_observation is not None and observation is not None) log = sbibm.get_logger(__name__) db = "sqlite:///" + os.path.join( tempfile.gettempdir(), f"pyabc_{time.time()}_{random.randint(0, 1e9)}.db") # Wrap sbibm prior and simulator for pyABC prior = wrap_prior(task) simulator = PyAbcSimulator(task) distance_str = distance if observation is None: observation = task.get_observation(num_observation) # Population size strategy if population_size is None: population_size = 100 if num_simulations > 10_000: population_size = 1000 # Find initial epsilon with rej abc run. initial_round_size = clip_int( value=initial_round_factor * population_size, minimum=population_size, maximum=max(0.5 * num_simulations, population_size), ) log.info( f"Running REJ-ABC with {initial_round_size} samples to find initial epsilon." ) _, distances = run_rejection_abc(task, initial_round_size, population_size, observation, distance_str, batch_size) initial_epsilon = distances[-1].item() # Wrap observation and distance for pyabc. distance = get_distance(distance_str) observation = np.atleast_1d(np.array(observation, dtype=float).squeeze()) # Define quantile based epsilon decay. epsilon = pyabc.epsilon.QuantileEpsilon(initial_epsilon=initial_epsilon, alpha=epsilon_decay) # Perturbation kernel transition = pyabc.transition.MultivariateNormalTransition( scaling=kernel_variance_scale) population_size = min(population_size, num_simulations) if population_strategy == "constant": population_size_strategy = population_size elif population_strategy == "adaptive": raise NotImplementedError("Not implemented atm.") population_size_strategy = pyabc.populationstrategy.AdaptivePopulationSize( start_nr_particles=population_size, max_population_size=int(10 * population_size), min_population_size=int(0.1 * population_size), ) # Multiprocessing if num_workers > 1: sampler = pyabc.sampler.MulticoreParticleParallelSampler( n_procs=num_workers) else: sampler = pyabc.sampler.SingleCoreSampler(check_max_eval=False) # Collect kwargs kwargs = dict( parameter_priors=[prior], distance_function=distance, population_size=population_size_strategy, transitions=[transition], eps=epsilon, sampler=sampler, ) # Semi-automatic summary statistics. if sass: num_pilot_simulations = int(sass_fraction * num_simulations) log.info(f"SASS pilot run with {num_pilot_simulations} simulations.") kwargs["models"] = [simulator] # Run pyabc with fixed budget. pilot_theta, pilot_weights = run_pyabc( task, db, num_pilot_simulations, observation, pyabc_kwargs=kwargs, use_last_pop_samples=use_last_pop_samples, distance_str=distance_str, batch_size=batch_size, ) # Regression # TODO: Posterior does not return xs, which we would need for # regression adjustment. So we will resimulate, which is # unneccessary. Should ideally change `inference_method` to return xs # if requested instead. This step thus does not count towards budget pilot_x = task.get_simulator(max_calls=None)(pilot_theta) # Run SASS. sumstats_transform = get_sass_transform( theta=pilot_theta, x=pilot_x, expansion_degree=sass_feature_expansion_degree, sample_weight=pilot_weights if sass_sample_weights else None, ) # Update simulator to use sass summary stats. def sumstats_simulator(theta): # Pyabc simulator returns dict. x = simulator(theta)["data"].reshape(1, -1) # Transform return Tensor. sx = sumstats_transform(x) return dict(data=sx.numpy().squeeze()) observation = sumstats_transform(observation.reshape(1, -1)) observation = np.atleast_1d( np.array(observation, dtype=float).squeeze()) log.info(f"Finished learning summary statistics.") else: sumstats_simulator = simulator num_pilot_simulations = 0 population_size = min(population_size, num_simulations) log.info("""Running ABC-SMC-pyabc with {} simulations""".format( num_simulations - num_pilot_simulations)) kwargs["models"] = [sumstats_simulator] # Run pyabc with fixed budget. particles, weights = run_pyabc( task, db, num_simulations=num_simulations - num_pilot_simulations, observation=observation, pyabc_kwargs=kwargs, use_last_pop_samples=use_last_pop_samples, distance_str=distance_str, batch_size=batch_size, ) if lra: log.info(f"Running linear regression adjustment.") # TODO: Posterior does not return xs, which we would need for # regression adjustment. So we will resimulate, which is # unneccessary. Should ideally change `inference_method` to return xs # if requested instead. xs = task.get_simulator(max_calls=None)(particles) # NOTE: If posterior is bounded we should do the regression in # unbounded space, as described in https://arxiv.org/abs/1707.01254 transform_to_unbounded = True transforms = task._get_transforms(transform_to_unbounded)["parameters"] # Update the particles with LRA. particles = run_lra( theta=particles, x=xs, observation=torch.tensor(observation, dtype=torch.float32).unsqueeze(0), sample_weight=weights if lra_sample_weights else None, transforms=transforms, ) # TODO: Maybe set weights uniform because they can't be updated? # weights = torch.ones(particles.shape[0]) / particles.shape[0] if kde_bandwidth is not None: samples = particles log.info( f"KDE on {samples.shape[0]} samples with bandwidth option {kde_bandwidth}" ) kde = get_kde( samples, bandwidth=kde_bandwidth, sample_weight=weights if kde_sample_weights else None, ) samples = kde.sample(num_samples) else: log.info(f"Sampling {num_samples} samples from trace") samples = sample_with_weights(particles, weights, num_samples=num_samples) log.info(f"Unique samples: {torch.unique(samples, dim=0).shape[0]}") return samples, simulator.simulator.num_simulations, None
def run( task: Task, num_samples: int, num_simulations: int, num_simulations_per_step: int = 100, num_observation: Optional[int] = None, observation: Optional[torch.Tensor] = None, automatic_transforms_enabled: bool = False, mcmc_method: str = "slice_np", mcmc_parameters: Dict[str, Any] = {}, diag_eps: float = 0.0, ) -> (torch.Tensor, int, Optional[torch.Tensor]): """Runs (S)NLE from `sbi` Args: task: Task instance num_observation: Observation number to load, alternative to `observation` observation: Observation, alternative to `num_observation` num_samples: Number of samples to generate from posterior num_simulations: Simulation budget num_simulations_per_step: Number of simulations per MCMC step automatic_transforms_enabled: Whether to enable automatic transforms mcmc_method: MCMC method mcmc_parameters: MCMC parameters diag_eps: Epsilon applied to diagonal Returns: Samples from posterior, number of simulator calls, log probability of true params if computable """ assert not (num_observation is None and observation is None) assert not (num_observation is not None and observation is not None) log = logging.getLogger(__name__) log.info(f"Running SL") prior = task.get_prior_dist() if observation is None: observation = task.get_observation(num_observation) simulator = task.get_simulator() transforms = task._get_transforms(automatic_transforms_enabled)["parameters"] prior = wrap_prior_dist(prior, transforms) simulator = wrap_simulator_fn(simulator, transforms) likelihood_estimator = SynthLikNet( simulator=simulator, num_simulations_per_step=num_simulations_per_step, diag_eps=diag_eps, ) posterior = LikelihoodBasedPosterior( method_family="snle", neural_net=likelihood_estimator, prior=prior, x_shape=observation.shape, mcmc_parameters=mcmc_parameters, ) posterior.set_default_x(observation) posterior = wrap_posterior(posterior, transforms) # assert simulator.num_simulations == num_simulations samples = posterior.sample((num_samples,)).detach() return samples, simulator.num_simulations, None