Advanced Features

This part of documentation lists other features provided alongside or inside Fitter and Inferencer objects to allow users easier and a more flexible development when working on their own problems.

Parameters initialization

Whilst running Fitter or Inferencer, the user is able to pass the values of the parameters and variables that will be used as initial conditions when solving the differential equations defined in the neuron model.

Initial conditions should be passed by using an additional dictionary to the constructor:

init_conds = {'v': -30*mV}
fitter = TraceFitter(..., param_init = init_conds)

or

inferencer = Inferencer(..., param_init=init_conds)

Restart

By default any Fitter object works in continuous optimization mode between run, where all of the parameters drawn are being evaluated.

By setting the restart argument in fit() to True, the user can restart the optimizer and the optimization will start from scratch.

Used by Fitter optimizer and metric can only be changed when the flat is True.

The previously outlined restart argument is used in the similar fashion in infer() method. It is set to False by default, and each following re-call of the method will result in the multi-round inference. If the user wants amortized inference without using any knowledge from the previous round of optimization instead, the restart argument should be set to True.

Multi-objective optimization

In the case of Fitter classes, it is possible to fit more than one output variable at the same time by combining the errors for each variable. To do so, the user can specify several output variables during the initialization as follows:

fitter = TraceFitter(...,
                     output={'x': target_x,
                             'y': target_y})

If the fitter function uses a single metric, it is applied to both variables.

Note

This approach requires that the resulting error has the same units for all variables, i.e., it would not be possible to use the same MSEMetric on variables with different units, since the errors cannot be simply added up.

As a more general solution, the user can specify a metric for each variable and utilize their normalization arguments to make the units compatible (most commonly by turning both errors into dimensionless quantities). The normalization also defines the relative weights of all errors. For example, if the variable x has dimensions of mV and the variable y is dimensionless, the following metrics can be used to make an error of 10 mV in x to be weighed as much as an error of 0.1 in y

metrics = {'x': MSEMetric(normalization=10*mV),
           'y': MSEMetric(normalization=0.1)}

This has to be passed as the metric argument of the fit function.

In the case of the Inferencer class, switching from a single- to multi-objective optimization is seamless. The user has to provide multiple output variables during the initialization process the same way as for Fitter classes:

inferencer = Inferencer(...,
                        output={'x': target_x,
                                'y': target_y})

Later, during the inference process, the user has to define feautres for each output variable as follows:

posterior = inferencer.infer(...,
                             features={'x': list_of_features_for_x,
                                       'y': list_of_features_for_y})

If the user prefers automatic feature extraction, the features argument should not be defined (it should stay set to None).

Warning

If the user chooses to define a list of features for extracting the summary features, it is important to keep in mind that the total number of features will be increased as many times as there are output variables set for multi-objective optimization.

Callback function

To visualize the progress of the optimization we provided few possibilities of the feedback inside the Fitter.

The ‘callback’ input provides few default options, updated in each round:
  • 'text' (default) - prints out the parameters of the best fit and corresponding error;
  • 'progressbar' - uses tqdm.autonotebook to provide a progress bar;
  • None - non-verbose;

as well as customized feedback option. User can provide a callable (i.e., a function), that ensures either returning an output or printout. If callback returns True, the fitting execution will be interrupted.

User gets four arguments to customize over:
  • params - set of parameters from current round;
  • errors - set of errors from current round;
  • best_params - best parameters globally, from all rounds;
  • best_error - best parameters globally, from all rounds;
  • index - index of current round.

An example callback function:

def callback_fun(params, errors, best_params, best_error, index):
    print('index {} errors minimum: {}'.format(index, min(errors)))

...

fitter = TraceFitter(...)
result, error  = fitter.fit(..., callback=callback_fun)

OnlineTraceFitter

OnlineTraceFitter was created to work with long traces or large-scale optimization problems. This Fitter class uses online mean square error as a metric. When the fit() method is called there is no need of specifying a metric, which is by default set to None. The errors are instead calculated with run_regularly for each simulation.

fitter = OnlineTraceFitter(model=model,
                           input={'I': inp_traces},
                           output={'v': out_traces},
                           dt=0.1*ms,
                           n_samples=5)

result, error = fitter.fit(optimizer=optimizer,
                           n_rounds=1,
                           gl=[1e-8*siemens*cm**-2 * area, 1e-3*siemens*cm**-2 * area])

Reference the target values in the equations

A model can refer to the target output values within the equations. For example, if the membrane potential trace v (i.e. output_var='v') is used for the optimization, equations can refer to the target trace as v_target. This allows adding a coupling term such as: coupling*(v_target - v) to the equation that corresponds to state variable v, pulling the trajectory towards the correct solution.

Generate Traces

Fitter and Inferencer classes allow the user can to generate the traces with optimized parameters.

For a quick access to best fitted set of parameters Fitter classes provide ready to use functions:

These functions can be called after the fitting procedure is finalized in the following manner, without any input arguments:

fitter = TraceFitter(...)
results, error = fitter.fit(...)
traces = fitter.generate_traces()
fitter = SpikeFitter(...)
results, error = fitter.fit(...)
spikes = fitter.generate_spikes()

On the other hand, since the Inferencer class is able to perform the inference of the unknown parameter distribution by utilizing output traces and spike trains simultaneously, generate_traces is used for both.

Once the approximated posterior distribution is built, the user is allowed to call generate_traces on Inferencer object. If only one output variable is used for the optimization of the parameters, the user does not have to specifiy output variable in the generate_traces method through output_var argument. If, for example, the multi-objective optimization is performed by using both output traces and spike trains and the user is interested in only times of spike events, output_var should be set to 'spike'. Otherwise, if the user specifies a list of names or the output_var is not specified, a dictionary with keys set to output variable names and with their respective values, will be returned instead.

Customize the generate method for Fitter

To create traces for other parameters, or generate traces after the spike train fitting, user can call the generate method, which takes in the following arguments:

fitter.generate(params=..., output_var=..., param_init=..., level=0)

where params should be a dictionary of parameters for which we generate the traces; output_var provides an option to pick one or more variables for visualization; with param_init, the user is able to define the initial values for differential equations in the model; and level allows for specification of the namespace level from which we are able to get the constant parameters of the model.

If output_var is the name of a single variable name (or the special name 'spikes'), a single Quantity (for variables) or a list of spikes time arrays (for 'spikes') will be returned. If a list of names is provided, then the result is a dictionary with all the results.

fitter = TraceFitter(...)
results, error = fitter.fit(...)
traces = fitter.generate(output_var=['v', 'h', 'n', 'm'])
v_trace = traces['v']
h_trace = traces['h']

Results

Fitter classes store all of the parameters used by the optimizer as well as the corresponding errors. To retrieve them you can call the results.

fitter = TraceFitter(...)
...
traces = fitter.generate_traces()
fitter = SpikeFitter(...)
...
results = fitter.results(format='dataframe')

Results can be returned in one of the following formats:

  • 'list' (default) - returns a list of dictionaries with corresponding parameters (including units) and errors;
  • 'dict' - returns a dictionary of arrays with corresponding parameters (including units) and errors;
  • 'dataframe' - returns a DataFrame (without units).

The use of units (only relevant for formats 'list' and 'dict') can be switched on or off with the use_units argument. If it is not specified, it will default to the value used during the initialization of the Fitter (which itself defaults to True).

Example output:

  • format='list':
[{'gl': 80.63365773 * nsiemens, 'g_kd': 66.00430921 * usiemens, 'g_na': 145.15634566 * usiemens, 'errors': 0.00019059452295872703},
 {'gl': 83.29319947 * nsiemens, 'g_kd': 168.75187749 * usiemens, 'g_na': 130.64547027 * usiemens, 'errors': 0.00021434415430605653},
 ...]
  • format='dict':
{'g_na': array([176.4472297 , 212.57019659, ...]) * usiemens,
 'g_kd': array([ 43.82344525,  54.35309635, ...]) * usiemens,
 'gl': array([ 69.23559876, 134.68463669, ...]) * nsiemens,
 'errors': array([1.16788502, 0.5253008 , ...])}
  • format='dataframe':
   g_na      gl            g_kd      errors
0  0.000280  8.870238e-08  0.000047  0.521425
1  0.000192  1.121861e-07  0.000118  0.387140
...

Posterior distribution analysis

Unlike Fitter classes, the Inferencer class does not keep track of all parameter values. Rather, it stores all training data for neural density estimator which will later be used for building the posterior distribution of each unknown parameter. Thus, the Inferencer does not returns best-fit values and corresponding errors, but the entire posterior distribution that can be used to draw samples from, compute descriptive statistics of parameters, analyize pairwise relationship between each to parameters, etc.

There are three methods that enable the comprehensive analysis of the posterior:

  • pairplot - returns axes of drawn samples from the posterior in a 2-dimenstional grid with marginals and pairwise marginals. Using this method, the user is able to inspect the relationship for all combinations of distributions for each parameter;
  • conditional_pairplot - visualizes the conditional pairplot;
  • conditional_corrcoeff - returns the correlation matrix of a distribution conditioned with the user-specified condition.

To see this in action, go to our tutorial page and learn how to use each of these methods.

Standalone mode

Just like with regular Brian 2 scripts, all computations in the toolbox can be performed in Runtime mode (default) or Standalone mode. For details, please check the official Brian 2 documentation: https://brian2.readthedocs.io/en/stable/user/computation.html

To enable the Standalone mode, and to allowthe source code generation to C++ code, add the following code right after Brian 2 is imported, but before the simulation code:

set_device('cpp_standalone')

Important notes:

Warning

In the Standalone mode, a single script should not contain multiple Fitter or Inferencer classes. Please, use separate scripts.

Note that the generation of traces or spikes via generate will always use runtime mode, even when the fitting procedure uses standalone mode.

Embedding network for automatic feature extraction

If the features argument of the Inferencer class is not defined, automatic feature extraction from the given output traces will occur. By default, this is done by using the multi-layer perceptron that is trained in parallel with the neural density estimator of choice during the inference process. If the user wants to specify their own custom embedding network, it is possible to do so by creating a neural network by using PyTorch library and passing the instance of that neural network as an additional keyword argument as follows:

import torch
from torch import nn

...

class CustomEmbeddingNet(nn.Module):

    def __init__(self, in_features, out_features, ...):
        ...

    def forward(self, x):
        ...


in_features = out_traces.shape[1]
out_features = ...
embedding_net = CustomEmbeddingNet(in_features, out_features, ...)

...

inferencer = Inferencer(...)
inferencer.infer(...,
                 inference_kwargs={'embedding_net': embedding_net})

GPU usage for inference

It is possible to use the GPU for training the sdensity estimator. It is enough to specify the sbi_device to 'gpu' or 'cuda'. Otherwise, if not specified, or if set to 'cpu', training will be done by using the CPU.

Note

For default density estimators that are used either for SNPE, SNLE and SNRE, there are no significant speed-ups expected if the training is translocated to the GPU.

It is, however, possible to achieve a significant speed-up if the custom embedding network relies on convolutions to extract feautres. Such operations are known to achieve improvement in compuation time multifold.