Model implementation and execution with Scikit-NeuroMSI
This tutorial covers the basic pipeline for implementing and running multisensory integration models using Scikit-NeuroMSI. We show how to implement the different models classes currently supported by the package, organized in the mle, bayesian and neural modules. It covers the execution objects and methods that are shared by all the models of the package: run and NDResult.
Note: In this tutorial we assume that you already have a basic knowledge of
numpyfor scientific computing.
Implementation of Maximum Likelihood Estimation (MLE) models
To easily implement the model developed by Alais and Burr (2004) you can import the corresponding module and instatiate the AlaisBurr2004 class:
[2]:
from skneuromsi.mle import AlaisBurr2004
model = AlaisBurr2004()
model
[2]:
<skneuromsi.mle._alais_burr2004.AlaisBurr2004 at 0x708e7e48ce50>
Exploration of the model object
By calling the model using the vars function you can explore its main built-in parameters and methods:
[3]:
vars(model)
[3]:
{'_mode0': 'auditory',
'_mode1': 'visual',
'_position_range': (-20, 20),
'_position_res': 0.01,
'_time_range': (1, 1),
'_time_res': 1.0,
'_random': Generator(PCG64) at 0x708DC82435A0,
'run': <function skneuromsi.mle._alais_burr2004.AlaisBurr2004.run(*, auditory_position=-5, visual_position=5, auditory_sigma=3.0, visual_sigma=3.0, noise=None)>}
mode0: Name of the first sensory modality defined in the model.mode1: Name of the second sensory modality defined in the model.position_range: The range of possible positions where the stimulus could be delivered (in degrees).position_res: The resolution of the range of possible positions where the stimulus could be delivered (in degrees).time_range: The range of possible times when the stimulus could be delivered. Here is set to 1 because the model has no temporal dimension.time_res: The resolution of the range of possible times when the stimulus could be delivered.random: Embedded random number generator (useful to include noise in the model responses).run: Executes the model and saves the result.
We can re-implement Alais and Burr (2004) model with different sensory modalities and position ranges by modifying the initial parameters of the model:
[8]:
model = AlaisBurr2004(
mode0="visual", mode1="proprioceptive", position_range=(-50, 50)
)
vars(model)
[8]:
{'_mode0': 'visual',
'_mode1': 'proprioceptive',
'_position_range': (-50, 50),
'_position_res': 0.01,
'_time_range': (1, 1),
'_time_res': 1.0,
'_random': Generator(PCG64) at 0x708DC7FA1540,
'run': <function skneuromsi.mle._alais_burr2004.AlaisBurr2004.run(*, visual_position=-5, proprioceptive_position=5, visual_sigma=3.0, proprioceptive_sigma=3.0, noise=None)>}
The model object has a built-in run method:
[11]:
model.run
[11]:
<function skneuromsi.mle._alais_burr2004.AlaisBurr2004.run(*, visual_position=-5, proprioceptive_position=5, visual_sigma=3.0, proprioceptive_sigma=3.0, noise=None)>
By calling this method we can observe its arguments:
visual_position: The position where the visual stimulus is delivered (in degrees).proprioceptive_position: The position where the propioceptive stimulus is delivered (in degrees).visual_sigma: Standard deviation of the auditory estimate.proprioceptive_sigma: Standard deviation of the propioceptive estimate.
Now let’s run the model for equidistant auditory and visual locations:
[13]:
res = model.run(visual_position=-5, proprioceptive_position=5)
res
[13]:
<NDResult 'AlaisBurr2004', modes=['visual' 'proprioceptive' 'multi'], times=1, positions=10000, positions_coordinates=1, causes=False>
The model outputs one NDResult object containing the results of both unisensory estimators and the multisensory estimator.
Refer to the API documentation for further information about the parameters to manipulate in this model.
Exploration of the results object
By calling the NDResult object using the vars function you can explore its main built-in parameters and methods:
[14]:
vars(res)
[14]:
{'_mname': 'AlaisBurr2004',
'_mtype': 'MLE',
'_output_mode': 'multi',
'_nmap': {'auditory': 'visual',
'visual': 'proprioceptive',
'auditory_weight': 'visual_weight',
'visual_weight': 'proprioceptive_weight'},
'_time_range': array([1, 1]),
'_position_range': array([-50, 50]),
'_time_res': 1.0,
'_position_res': 0.01,
'_run_parameters': {'visual_position': -5,
'proprioceptive_position': 5,
'visual_sigma': 3.0,
'proprioceptive_sigma': 3.0,
'noise': None},
'_extra': {'visual_weight': 0.5, 'proprioceptive_weight': 0.5},
'_causes': None,
'_nddata': <xarray.DataArray 'values' (modes: 3, times: 1, positions: 10000,
positions_coordinates: 1)> Size: 240kB
array([[[[1.84356985e-050],
[1.93808093e-050],
[2.03741451e-050],
...,
[1.65115301e-074],
[1.55331368e-074],
[1.46125559e-074]]],
[[[1.37463811e-074],
[1.46125559e-074],
[1.55331368e-074],
...,
[2.14181549e-050],
[2.03741451e-050],
[1.93808093e-050]]],
[[[4.33458701e-122],
[4.84392981e-122],
[5.41300350e-122],
...,
[6.04879857e-122],
[5.41300350e-122],
[4.84392981e-122]]]])
Coordinates:
* modes (modes) <U14 168B 'visual' 'proprioceptive' 'multi'
* times (times) int64 8B 0
* positions (positions) int64 80kB 0 1 2 3 ... 9997 9998 9999
* positions_coordinates (positions_coordinates) <U2 8B 'x0'}
We observe metadata related to the model that was executed to generate the results object, as well as the model execution (run) parameters. Notably the NDResult object holds useful methods for exploring the results of the model execution: get_modes, stats and plot.
Now let’s explore the get_modes method:
[37]:
res.get_modes()
[37]:
| modes | visual | proprioceptive | multi | ||
|---|---|---|---|---|---|
| times | positions | positions_coordinates | |||
| 0 | 0 | x0 | 1.843570e-50 | 1.374638e-74 | 4.334587e-122 |
| 1 | x0 | 1.938081e-50 | 1.461256e-74 | 4.843930e-122 | |
| 2 | x0 | 2.037415e-50 | 1.553314e-74 | 5.413003e-122 | |
| 3 | x0 | 2.141815e-50 | 1.651153e-74 | 6.048799e-122 | |
| 4 | x0 | 2.251541e-50 | 1.755135e-74 | 6.759122e-122 | |
| ... | ... | ... | ... | ... | |
| 9995 | x0 | 1.865646e-74 | 2.366862e-50 | 7.552692e-122 | |
| 9996 | x0 | 1.755135e-74 | 2.251541e-50 | 6.759122e-122 | |
| 9997 | x0 | 1.651153e-74 | 2.141815e-50 | 6.048799e-122 | |
| 9998 | x0 | 1.553314e-74 | 2.037415e-50 | 5.413003e-122 | |
| 9999 | x0 | 1.461256e-74 | 1.938081e-50 | 4.843930e-122 |
10000 rows × 3 columns
This method returns a pandas DataFrame with the output values for each modality at each time point and spatial coordinates. In the package we called such output values simply as values, given that the outputs are different across models. In this MLE model, the output values are probability density values.
Note: This model does not include the temporal dimension, hence here is represented as a fixed time point. This model does include spatial dimensions (generically named
positionsin the package). The package allows to have more than one spatial dimension (in the package calledposition_coordinates). In this model we have a single spatial dimension, defined as aposition_coordinatenamedx0by default.
Now let’s explore the stats method:
[47]:
res.stats()
[47]:
| describe | |
|---|---|
| count | 3.000000e+04 |
| mean | 1.000000e-02 |
| std | 3.114718e-02 |
| min | 4.334587e-122 |
| 25% | 3.091076e-43 |
| 50% | 4.385153e-20 |
| 75% | 3.429870e-06 |
| max | 1.880632e-01 |
By default it provides descriptive statistics of the model output. We can have more specific statistics by specifying the kind and modes argument, for example:
[49]:
res.stats(kind="max", modes="visual")
[49]:
0.1329807601338109
A useful method is the dimmax, which provides the maximum value observed in a specific modality and its temporal and spatial coordinates:
[35]:
res.stats.dimmax(modes="visual")
[35]:
times 0
positions 4500
positions_coordinates x0
values 0.1329807601338109
Name: max, dtype: object
Refer to the API documentation for further information about the NDResult object.
So far we have covered the fundamentals of model classes and results objects in the Scikit-NeuroMSI package. These are applicable to all model classes. Now we are going to introduce the
Bayesianandneuralmodules, and present some distinctive characteristics of the objects generated by this models.
Implementation of Bayesian models
You can implement the Causal Inference model developed by Kording et al. (2007) by importing the corresponding module and instantiating the Kording2007 class:
[65]:
from skneuromsi.bayesian import Kording2007
model = Kording2007(position_range=(-20, 20), position_res=1, n=100000)
model
[65]:
<skneuromsi.bayesian._kording2007.Kording2007 at 0x708db6520bb0>
[66]:
vars(model)
[66]:
{'_n': 100000,
'_mode0': 'auditory',
'_mode1': 'visual',
'_position_range': (-20, 20),
'_position_res': 1.0,
'_time_range': (0, 1),
'_time_res': 1.0,
'_random': Generator(PCG64) at 0x708DC5052C00,
'run': <function skneuromsi.bayesian._kording2007.Kording2007.run(*, auditory_position=-15, visual_position=15, auditory_sigma=2.0, visual_sigma=10.0, p_common=0.5, prior_sigma=20.0, prior_mu=0, strategy='averaging', noise=True, causes_kind='count', dimension='space')>}
Refer to the API documentation for more details about the available parameters for this model.
Let’s run the model for two conflicting stimulus locations:
[67]:
res = model.run(auditory_position=-15, visual_position=15, causes_kind="count")
res
[67]:
<NDResult 'Kording2007', modes=['auditory' 'visual' 'multi'], times=1, positions=40, positions_coordinates=1, causes=2>
In this model we have an output named causes, which provides information about the estimated number of causes (sources) of the stimuli presented to the model. This is a fundamental attribute of causal inference models of multisensory integration.
Let’s now explore the model output:
[68]:
res.get_modes()
[68]:
| modes | auditory | multi | visual | ||
|---|---|---|---|---|---|
| times | positions | positions_coordinates | |||
| 0 | 0 | x0 | 0.006784 | 0.001400 | 0.000000 |
| 1 | x0 | 0.022207 | 0.005452 | 0.000012 | |
| 2 | x0 | 0.056269 | 0.018036 | 0.000061 | |
| 3 | x0 | 0.107057 | 0.051357 | 0.000230 | |
| 4 | x0 | 0.165070 | 0.102585 | 0.000460 | |
| 5 | x0 | 0.195675 | 0.163996 | 0.001102 | |
| 6 | x0 | 0.185072 | 0.200408 | 0.002312 | |
| 7 | x0 | 0.134455 | 0.189935 | 0.004043 | |
| 8 | x0 | 0.076893 | 0.139467 | 0.005690 | |
| 9 | x0 | 0.035084 | 0.078207 | 0.008062 | |
| 10 | x0 | 0.011434 | 0.034802 | 0.010011 | |
| 11 | x0 | 0.003287 | 0.011054 | 0.012614 | |
| 12 | x0 | 0.000641 | 0.002821 | 0.013558 | |
| 13 | x0 | 0.000070 | 0.000440 | 0.015180 | |
| 14 | x0 | 0.000000 | 0.000040 | 0.016972 | |
| 15 | x0 | 0.000000 | 0.000000 | 0.018618 | |
| 16 | x0 | 0.000000 | 0.000000 | 0.019018 | |
| 17 | x0 | 0.000000 | 0.000000 | 0.020591 | |
| 18 | x0 | 0.000000 | 0.000000 | 0.022480 | |
| 19 | x0 | 0.000000 | 0.000000 | 0.024017 | |
| 20 | x0 | 0.000000 | 0.000000 | 0.025083 | |
| 21 | x0 | 0.000000 | 0.000000 | 0.026632 | |
| 22 | x0 | 0.000000 | 0.000000 | 0.028375 | |
| 23 | x0 | 0.000000 | 0.000000 | 0.030615 | |
| 24 | x0 | 0.000000 | 0.000000 | 0.032612 | |
| 25 | x0 | 0.000000 | 0.000000 | 0.034634 | |
| 26 | x0 | 0.000000 | 0.000000 | 0.035905 | |
| 27 | x0 | 0.000000 | 0.000000 | 0.039827 | |
| 28 | x0 | 0.000000 | 0.000000 | 0.042272 | |
| 29 | x0 | 0.000000 | 0.000000 | 0.044076 | |
| 30 | x0 | 0.000000 | 0.000000 | 0.045953 | |
| 31 | x0 | 0.000000 | 0.000000 | 0.046945 | |
| 32 | x0 | 0.000000 | 0.000000 | 0.049003 | |
| 33 | x0 | 0.000000 | 0.000000 | 0.048688 | |
| 34 | x0 | 0.000000 | 0.000000 | 0.050613 | |
| 35 | x0 | 0.000000 | 0.000000 | 0.048918 | |
| 36 | x0 | 0.000000 | 0.000000 | 0.048156 | |
| 37 | x0 | 0.000000 | 0.000000 | 0.046945 | |
| 38 | x0 | 0.000000 | 0.000000 | 0.042006 | |
| 39 | x0 | 0.000000 | 0.000000 | 0.037709 |
As in MLE models, the output values of the Bayesian models represent probability density values. Note that we also have a fixed time point.
Now let’s see what happens if we reduce the distance of the stimuli:
[58]:
res = model.run(auditory_position=-3, visual_position=3, causes_kind="count")
res
[58]:
<NDResult 'Kording2007', modes=['auditory' 'visual' 'multi'], times=1, positions=40, positions_coordinates=1, causes=1>
The model now reports that the stimuli as originating from a common source. We can directly observe the probability of the stimuli originating from a common cause:
[44]:
print("p(C=1):", res.e_["mean_p_common_cause"])
print("C:", res.causes_)
p(C=1): 0.561605322439197
C: 1
Refer to the API documentation for more details about the models available in the bayesian module.
This demonstration of the Bayesian Causal Inference model mechanics is inspired in the tutorial of the BCIT Toolbox.
Implementation of neural models
You can implement the network model developed by Cuppini et al. (2017) on by importing the corresponding module and instantiating the Cuppini2017 class:
[71]:
from skneuromsi.neural import Cuppini2017
model = Cuppini2017(neurons=90, position_range=(0, 90))
model
[71]:
<skneuromsi.neural._cuppini2017.Cuppini2017 at 0x708db6521180>
[72]:
vars(model)
[72]:
{'_neurons': 90,
'_position_range': (0, 90),
'_position_res': 1.0,
'_time_range': (0, 100),
'_time_res': 0.01,
'_integrator': <brainpy._src.integrators.ode.explicit_rk.Euler at 0x708db6434430>,
'_random': Generator(PCG64) at 0x708DC5052880,
'_mode0': 'auditory',
'_mode1': 'visual',
'run': <function skneuromsi.neural._cuppini2017.Cuppini2017.run(*, auditory_position=None, visual_position=None, auditory_sigma=32, visual_sigma=4, auditory_intensity=28, visual_intensity=27, auditory_duration=None, auditory_onset=0, auditory_stim_n=1, visual_duration=None, visual_onset=0, visual_stim_n=1, auditory_soa=None, visual_soa=None, noise=False, noise_level=0.4, feedforward_weight=18, cross_modal_weight=1.4, causes_kind='count', causes_dim='space', causes_peak_threshold=0.15, causes_peak_distance=None)>}
You can refer to the API documentation for more details about the available parameters.
As before, let’s run the model for two conflicting stimulus locations:
[73]:
res = model.run(auditory_position=30, visual_position=60)
[74]:
vars(res)
[74]:
{'_mname': 'Cuppini2017',
'_mtype': 'Neural',
'_output_mode': 'multi',
'_nmap': {'auditory': 'auditory', 'visual': 'visual'},
'_time_range': array([ 0, 100]),
'_position_range': array([ 0, 90]),
'_time_res': 0.01,
'_position_res': 1.0,
'_run_parameters': {'auditory_position': 30,
'visual_position': 60,
'auditory_sigma': 32,
'visual_sigma': 4,
'auditory_intensity': 28,
'visual_intensity': 27,
'auditory_duration': None,
'auditory_onset': 0,
'auditory_stim_n': 1,
'visual_duration': None,
'visual_onset': 0,
'visual_stim_n': 1,
'auditory_soa': None,
'visual_soa': None,
'noise': False,
'noise_level': 0.4,
'feedforward_weight': 18,
'cross_modal_weight': 1.4,
'causes_kind': 'count',
'causes_dim': 'space',
'causes_peak_threshold': 0.15,
'causes_peak_distance': None},
'_extra': {'causes_kind': 'count',
'causes_dim': 'space',
'causes_peak_threshold': 0.15,
'causes_peak_distance': None,
'stim_position': [30, 60]},
'_causes': 2,
'_nddata': <xarray.DataArray 'values' (modes: 3, times: 10000, positions: 90,
positions_coordinates: 1)> Size: 22MB
array([[[[1.19097530e-03],
[1.31460645e-03],
[1.44163460e-03],
...,
[8.54958672e-04],
[9.59982932e-04],
[1.07232604e-03]],
[[2.24271407e-03],
[2.48409320e-03],
[2.73379601e-03],
...,
[1.59484536e-03],
[1.79605986e-03],
[2.01258518e-03]],
[[3.17001717e-03],
[3.52222241e-03],
[3.88888969e-03],
...,
...
...,
[2.31613388e-03],
[2.33042543e-03],
[2.33972890e-03]],
[[2.34435053e-03],
[2.34445919e-03],
[2.34007850e-03],
...,
[2.31613358e-03],
[2.33042520e-03],
[2.33972873e-03]],
[[2.34435040e-03],
[2.34445910e-03],
[2.34007844e-03],
...,
[2.31613328e-03],
[2.33042497e-03],
[2.33972856e-03]]]])
Coordinates:
* modes (modes) <U8 96B 'auditory' 'visual' 'multi'
* times (times) int64 80kB 0 1 2 3 4 ... 9996 9997 9998 9999
* positions (positions) int64 720B 0 1 2 3 4 5 ... 85 86 87 88 89
* positions_coordinates (positions_coordinates) <U2 8B 'x0'}
The network models are characterized for having multiple parameters that define the network behavior. Also, these models exploit the temporal dimension since the neural activity is computed as a numerical integration at every time step:
[75]:
res.get_modes()
[75]:
| modes | auditory | multi | visual | ||
|---|---|---|---|---|---|
| times | positions | positions_coordinates | |||
| 0 | 0 | x0 | 0.001191 | 0.000025 | 0.000002 |
| 1 | x0 | 0.001315 | 0.000025 | 0.000002 | |
| 2 | x0 | 0.001442 | 0.000025 | 0.000002 | |
| 3 | x0 | 0.001570 | 0.000025 | 0.000002 | |
| 4 | x0 | 0.001699 | 0.000025 | 0.000002 | |
| ... | ... | ... | ... | ... | ... |
| 9999 | 85 | x0 | 0.000267 | 0.002271 | 0.000015 |
| 86 | x0 | 0.000309 | 0.002296 | 0.000015 | |
| 87 | x0 | 0.000356 | 0.002316 | 0.000015 | |
| 88 | x0 | 0.000412 | 0.002330 | 0.000015 | |
| 89 | x0 | 0.000476 | 0.002340 | 0.000016 |
900000 rows × 3 columns
The output values of this models represent neural activity values. Note that the model output now haves multiple time points, so we have the neural activity values at each time point of the simluation for each spatial coordinate encoded in the network.
We observe that the model detects two distinct causes from the stimuli by looking at the causes output of the model:
[76]:
print("C:", res.causes_)
C: 2
Now let’s see what happens if we reduce the distance of the stimuli:
[78]:
res = model.run(auditory_position=40, visual_position=50)
res
[78]:
<NDResult 'Cuppini2017', modes=['auditory' 'visual' 'multi'], times=10000, positions=90, positions_coordinates=1, causes=1>
[79]:
print("C:", res.causes_)
C: 1
The model outputs a common cause for stimuli close in space.
Refer to the API documentation for more details about the models available in the neural module.