2.4. Searchlight : finding voxels containing information

2.4.1. Principle of the Searchlight

Searchlight was introduced in Information-based functional brain mapping, Nikolaus Kriegeskorte, Rainer Goebel and Peter Bandettini (PNAS 2006) and consists in scanning the images volume with a searchlight. Briefly, a ball of given radius is scanned across the brain volume and the prediction accuracy of a classifier trained on the corresponding voxels is measured.

2.4.2. Preparing the data

2.4.2.1. Loading

Fetching the data from internet and loading it can be done with the provided functions (see Loading data):

import numpy as np
import nibabel
from nilearn import datasets
from nilearn.image import new_img_like

haxby_dataset = datasets.fetch_haxby_simple()

# print basic information on the dataset
print('Anatomical nifti image (3D) is located at: %s' % haxby_dataset.mask)
print('Functional nifti image (4D) is located at: %s' % haxby_dataset.func)

fmri_filename = haxby_dataset.func
fmri_img = nibabel.load(fmri_filename)
y, session = np.loadtxt(haxby_dataset.session_target).astype('int').T
conditions = np.recfromtxt(haxby_dataset.conditions_target)['f0']

2.4.2.2. Reshaping the data

For this example we need:

  • to put X in the form n_samples x n_features
  • compute a mean image for visualization background
  • limit our analysis to the face and house conditions (like in the decoding tutorial)
from nilearn.image import index_img
condition_mask = np.logical_or(conditions == b'face', conditions == b'house')

fmri_img = index_img(fmri_img, condition_mask)
y, session = y[condition_mask], session[condition_mask]
conditions = conditions[condition_mask]

2.4.2.3. Masking

One of the main elements that distinguish Searchlight from other algorithms is the notion of structuring element that scans the entire volume. If this seems rather intuitive, it has in fact an impact on the masking procedure.

Most of the time, fMRI data is masked and then given to the algorithm. This is not possible in the case of Searchlight because, to compute the score of non-masked voxels, some masked voxels may be needed. This is why two masks will be used here :

  • mask_img is the anatomical mask
  • process_mask_img is a subset of mask and contains voxels to be processed.

process_mask_img will then be used to restrain computation to one slice, in the back of the brain. mask_img will ensure that no value outside the brain is taken into account when iterating with the sphere.

mask_img = nibabel.load(haxby_dataset.mask)

# .astype() makes a copy.
process_mask = mask_img.get_data().astype(np.int)
picked_slice = 27
process_mask[..., (picked_slice + 1):] = 0
process_mask[..., :picked_slice] = 0
process_mask[:, 30:] = 0
process_mask_img = new_img_like(mask_img, process_mask)

2.4.3. Third Step: Setting up the searchlight

2.4.3.1. Classifier

The classifier used by default by SearchLight is LinearSVC with C=1 but this can be customed easily by passing an estimator parameter to the cross validation. See scikit-learn documentation for other classifiers.

2.4.3.2. Score function

Here we use precision as metrics to measure the proportion of true positives among all positive results for one class. Others metrics can be specified by the “scoring” argument to the SearchLight, as detailed in the scikit-learn documentation

2.4.3.3. Cross validation

SearchLight will iterate on the volume and give a score to each voxel. This score is computed by running a classifier on selected voxels. In order to make this score as accurate as possible (and avoid overfitting), a cross validation is made.

As SearchLight is computationally costly, we have chosen a cross validation method that does not take too much time. K-Fold along with K = 4 is a good compromise between running time and quality.

from sklearn.cross_validation import KFold
cv = KFold(y.size, n_folds=4)

2.4.4. Running Searchlight

Running SearchLight is straightforward now that everything is set. The only parameter left is the radius of the ball that will run through the data. Kriegskorte et al. use a 4mm radius because it yielded the best detection performance in their simulation.

searchlight = nilearn.decoding.SearchLight(
    mask_img,
    process_mask_img=process_mask_img,
    radius=5.6, n_jobs=n_jobs,
    verbose=1, cv=cv)
searchlight.fit(fmri_img, y)

2.4.5. Visualization

2.4.5.1. Searchlight

As the activation map is cropped, we use the mean image of all scans as a background. We can see here that voxels in the visual cortex contains information to distinguish pictures showed to the volunteers, which was the expected result.

decoding/../auto_examples/decoding/images/plot_haxby_searchlight_001.png
import matplotlib.pyplot as plt

# Use the fmri mean image as a surrogate of anatomical data
from nilearn import image
from nilearn.plotting import plot_stat_map
mean_fmri = image.mean_img(fmri_img)

plot_stat_map(new_img_like(mean_fmri, searchlight.scores_), mean_fmri,
              title="Searchlight", display_mode="z", cut_coords=[-16],
              colorbar=False)

### F_score results
p_ma = np.ma.array(p_unmasked, mask=np.logical_not(process_mask))
plot_stat_map(new_img_like(mean_fmri, p_ma), mean_fmri,
              title="F-scores", display_mode="z", cut_coords=[-16],
              colorbar=False)

plt.show()

2.4.5.2. Comparing to massively univariate analysis: F_score or SPM

The standard approach to brain mapping is performed using Statistical Parametric Mapping (SPM), using ANOVA (analysis of variance), and parametric tests (F-tests ot t-tests). Here we compute the p-values of the voxels [1]. To display the results, we use the negative log of the p-value.

decoding/../auto_examples/decoding/images/plot_haxby_searchlight_002.png
p_ma = np.ma.array(p_unmasked, mask=np.logical_not(process_mask))
plot_stat_map(new_img_like(mean_fmri, p_ma), mean_fmri,
              title="F-scores", display_mode="z", cut_coords=[-16],
              colorbar=False)

plt.show()

Parametric scores can be converted into p-values using a reference theoretical distribution, which is known under specific assumptions (hence the name parametric). In practice, neuroimaging signal has a complex structure that might not match these assumptions. An exact, non-parametric permutation test can be performed as an alternative to the parametric test: the residuals of the model are permuted so as to break any effect and the corresponding decision statistic is recomputed. One thus builds the distribution of the decision statistic under the hypothesis that there is no relationship between the tested variates and the target variates. In neuroimaging, this is generally done by swapping the signal values of all voxels while the tested variables remain unchanged [2]. A voxel-wise analysis is then performed on the permuted data. The relationships between the image descriptors and the tested variates are broken while the value of the signal in each particular voxel can be observed with the same probability than the original value associated to that voxel. Note that it is hereby assumed that the signal distribution is the same in every voxel. Several data permutations are performed (typically 10,000) while the scores for every voxel and every data permutation is stored. The empirical distribution of the scores is thus constructed (under the hypothesis that there is no relationship between the tested variates and the neuroimaging signal, the so-called null-hypothesis) and we can compare the original scores to that distribution: The higher the rank of the original score, the smaller is its associated p-value. The nilearn.mass_univariate.permuted_ols function returns the p-values computed with a permutation test.

from nilearn.mass_univariate import permuted_ols
# We use a two-sided t-test to compute p-values, but we keep trace of the
# effect sign to add it back at the end and thus observe the signed effect
neg_log_pvals, t_scores_original_data, _ = permuted_ols(
    grouped_conditions_encoded, grouped_fmri_masked,
    # + intercept as a covariate by default
    n_perm=10000, two_sided_test=True,
    n_jobs=1)  # can be changed to use more CPUs
signed_neg_log_pvals = neg_log_pvals * np.sign(t_scores_original_data)

The number of tests performed is generally large when full-brain analysis is performed (> 50,000 voxels). This increases the probability of finding a significant activation by chance, a phenomenon that is known to statisticians as the multiple comparisons problem. It is therefore recommended to correct the p-values to take into account the multiple tests. Bonferroni correction consists of multiplying the p-values by the number of tests (while making sure the p-values remain smaller than 1). Thus, we control the occurrence of one false detection at most, the so-called family-wise error control. A similar control can be performed when performing a permutation test: For each permutation, only the maximum value of the F-statistic across voxels is considered and is used to build the null distribution. It is crucial to assume that the distribution of the signal is the same in every voxel so that the F-statistics are comparable. This correction strategy is applied in nilearn nilearn.mass_univariate.permuted_ols function.

decoding/../auto_examples/manipulating_visualizing/images/plot_haxby_mass_univariate_001.png

We observe that the results obtained with a permutation test are less conservative than the ones obtained with a Bonferroni correction strategy.

In nilearn nilearn.mass_univariate.permuted_ols function, we permute a parametric t-test. Unlike F-test, a t-test can be signed (one-sided test), that is both the absolute value and the sign of an effect are considered. Thus, only positive effects can be focused on. It is still possible to perform a two-sided test equivalent to a permuted F-test by setting the argument two_sided_test to True. In the example above, we do perform a two-sided test but add back the sign of the effect at the end using the t-scores obtained on the original (non-permuted) data. Thus, we can perform two one-sided tests (a given contrast and its opposite) for the price of one single run. The example results can be interpreted as follows: viewing faces significantly activates the Fusiform Face Area as compared to viewing houses, while viewing houses does not reveals significant supplementary activations as compared to viewing faces.

[1]The p-value is the probability of getting the observed values assuming that nothing happens (i.e. under the null hypothesis). Therefore, a small p-value indicates that there is a small chance of getting this data if no real difference existed, so the observed voxel must be significant.
[2]When the variate tested is a scalar (test of the intercept) –which corresponds to a one sample test–, no swapping can be performed but one can estimate the null distribution by assuming symmetry about some reference value. When this value is zero, one can randomly swap the sign of the target variates (the imaging signal). nilearn nilearn.mass_univariate.permuted_ols function automatically adopts the suitable strategy according to the input data.