Repository: nilearn/nilearn

Issue:
space_net._crop_mask returns incomprehensible error when mask is empty
I have a user getting an error that he does not understand at all when the mask extraction has failed:

<pre>
C:\Users\LYF\AppData\Local\Enthought\Canopy\User\lib\site-packages\nilearn-0.1
.5.dev0-py2.7.egg\nilearn\decoding\space_net.pyc in path_scores(solver, X, y,
mask, alphas, l1_ratios, train, test, solver_params, is_classif, n_alphas,
eps, key, debias, Xmean, screening_percentile, verbose)
    356
    357     # crop the mask to have a tighter bounding box
--> 358     mask = _crop_mask(mask)
    359
    360     # get train and test data

C:\Users\LYF\AppData\Local\Enthought\Canopy\User\lib\site-packages\nilearn-0.1
.5.dev0-py2.7.egg\nilearn\decoding\space_net.pyc in _crop_mask(mask)
     61     the same support (active voxels)"""
     62     idx = np.where(mask)
---> 63     i_min = max(idx[0].min() - 1, 0)
     64     i_max = idx[0].max()
     65     j_min = max(idx[1].min() - 1, 0)

C:\Users\LYF\AppData\Local\Enthought\Canopy\User\lib\site-packages\numpy\core\
_methods.pyc in _amin(a, axis, out, keepdims)
     27
     28 def _amin(a, axis=None, out=None, keepdims=False):
---> 29     return umr_minimum(a, axis, None, out, keepdims)
     30
     31 def _sum(a, axis=None, dtype=None, out=None, keepdims=False):

ValueError: zero-size array to reduction operation minimum which has no
identity
</pre>


We should detect that "idx" is empty in _crop_mask and raise a more comprehensible error: something like "Empty mask: if you have given a mask, it is empty, and if you have not given a mask, the mask-extraction routines have fail. Please provide an appropriate mask".

@dohmatob : do you want to take care of that?



Discussion:
Oops! Ok, sure. Lemme reproduce and then fix it. Thanks.

Thanks!

Thanks for your work for my issue. I tried the updated code with a mask file, but the same error shows up.

There is my code:

# 

import numpy as np

# make X (design matrix) and y (response variable)
import nibabel
from nilearn.image import index_img

train_index = np.ones(60)>0
test_index = np.ones(600)>0

X_train = nibabel.load('C:\Users\LYF\Desktop\sim2train.nii')
X_test = nibabel.load('C:\Users\LYF\Desktop\sim2test.nii')

X_train = index_img(X_train,train_index)
X_test = index_img(X_test, test_index)

mask = nibabel.load('C:\Users\LYF\Desktop\sim1mask.nii')

a=np.array([1,2])
y_train = a.repeat(30)
y_test = a.repeat(300)

from nilearn.decoding import SpaceNetClassifier

penalty1 = 'graph-net'
penalty2 = 'tv-l1'
### Fit model on train data and predict on test data ######################
decoder = SpaceNetClassifier(penalty=penalty1,loss = 'logistic',screening_percentile=100,mask=mask)
decoder.fit(X_train, y_train)
y_pred = decoder.predict(X_test)
accuracy1 = (y_pred == y_test).mean() \* 100.
decoder = SpaceNetClassifier(penalty=penalty2,loss = 'logistic',screening_percentile=100,mask=mask)
decoder.fit(X_train, y_train)
y_pred = decoder.predict(X_test)
accuracy2 = (y_pred == y_test).mean() \* 100.

# 

And here is the track back log:

%run "C:\Users\LYF\Desktop\NILEARN\SIM1.py"
C:\Users\LYF\AppData\Local\Enthought\Canopy\User\lib\site-packages\sklearn\cross_validation.py:1601: DeprecationWarning: check_cv will return indices instead of boolean masks from 0.17
  'masks from 0.17', DeprecationWarning)
C:\Users\LYF\AppData\Local\Enthought\Canopy\User\lib\site-packages\sklearn\cross_validation.py:69: DeprecationWarning: The indices parameter is deprecated and will be removed (assumed True) in 0.17  

## stacklevel=1)

ValueError                                Traceback (most recent call last)
C:\Users\LYF\Desktop\NILEARN\SIM1.py in <module>()
     28 ### Fit model on train data and predict on test data ######################
     29 decoder = SpaceNetClassifier(penalty=penalty1,loss = 'logistic',screening_percentile=100)
---> 30 decoder.fit(X_train, y_train)
     31 y_pred = decoder.predict(X_test)
     32 accuracy1 = (y_pred == y_test).mean() \* 100.

C:\Users\LYF\Documents\GitHub\nilearn\nilearn\decoding\space_net.py in fit(self, X, y)
    829                 debias=self.debias, verbose=self.verbose,
    830                 screening_percentile=self.screening_percentile_
--> 831                 ) for cls in range(n_problems) for fold in range(n_folds)):
    832             self.best_model_params_.append((best_alpha, best_l1_ratio))
    833             self.alpha_grids_.append(alphas)

C:\Users\LYF\AppData\Local\Enthought\Canopy\User\lib\site-packages\sklearn\externals\joblib\parallel.pyc in **call**(self, iterable)
    657             self._iterating = True
    658             for function, args, kwargs in iterable:
--> 659                 self.dispatch(function, args, kwargs)
    660 
    661             if pre_dispatch == "all" or n_jobs == 1:

C:\Users\LYF\AppData\Local\Enthought\Canopy\User\lib\site-packages\sklearn\externals\joblib\parallel.pyc in dispatch(self, func, args, kwargs)
    404         """
    405         if self._pool is None:
--> 406             job = ImmediateApply(func, args, kwargs)
    407             index = len(self._jobs)
    408             if not _verbosity_filter(index, self.verbose):

C:\Users\LYF\AppData\Local\Enthought\Canopy\User\lib\site-packages\sklearn\externals\joblib\parallel.pyc in **init**(self, func, args, kwargs)
    138         # Don't delay the application, to avoid keeping the input
    139         # arguments in memory
--> 140         self.results = func(_args, *_kwargs)
    141 
    142     def get(self):

C:\Users\LYF\AppData\Local\Enthought\Canopy\User\lib\site-packages\sklearn\externals\joblib\memory.pyc in **call**(self, _args, *_kwargs)
    279 
    280     def **call**(self, _args, *_kwargs):
--> 281         return self.func(_args, *_kwargs)
    282 
    283     def call_and_shelve(self, _args, *_kwargs):

C:\Users\LYF\Documents\GitHub\nilearn\nilearn\decoding\space_net.py in path_scores(solver, X, y, mask, alphas, l1_ratios, train, test, solver_params, is_classif, n_alphas, eps, key, debias, Xmean, screening_percentile, verbose)
    353 
    354     # crop the mask to have a tighter bounding box
--> 355     mask = _crop_mask(mask)
    356 
    357     # get train and test data

C:\Users\LYF\Documents\GitHub\nilearn\nilearn\decoding\space_net.py in _crop_mask(mask)
     61     the same support (active voxels)"""
     62     idx = np.where(mask)
---> 63     i_min = max(idx[0].min() - 1, 0)
     64     i_max = idx[0].max()
     65     j_min = max(idx[1].min() - 1, 0)

C:\Users\LYF\AppData\Local\Enthought\Canopy\User\lib\site-packages\numpy\core_methods.pyc in _amin(a, axis, out, keepdims)
     27 
     28 def _amin(a, axis=None, out=None, keepdims=False):
---> 29     return umr_minimum(a, axis, None, out, keepdims)
     30 
     31 def _sum(a, axis=None, dtype=None, out=None, keepdims=False):

ValueError: zero-size array to reduction operation minimum which has no identity 

Can you put 'mask.nii' somewhere online, so that we have a look at it?

I cannot upload in the discussion at https://github.com/nilearn/nilearn/issues/822.https://github.com/nilearn/nilearn/issues/822

I have attached it in the email. And also the training data set and a brief description of the setting.

The mask is just ruling out the surface of the 20 by 20 by 10 box.

Here is a dropbox line for the three files I mentioned in the last reply.
https://www.dropbox.com/sh/pnj8kduyivvwmss/AABq_AL6WyB6ueMwoHnuyh75a?dl=0
Let me know if that does not work.
Thanks.

> I have attached it in the email.

Where? I am sorry, I missed it. If you need a temporary transfer service
to upload the file, we can provide that, just let us know.

> https://www.dropbox.com/sh/pnj8kduyivvwmss/AABq_AL6WyB6ueMwoHnuyh75a?dl=0

That works. Thanks

@Leo-YF-Liu : I am on it. It seems that it doesn't have much to do with your mask, but is a bug in our code. We'll have this fixed soon.

@GaelVaroquaux Thanks a lot. :)

Yes, it's a bug in our code, reproducible with:

```
import numpy as np
import nibabel
from nilearn.decoding import SpaceNetClassifier
X_train = nibabel.load("sim2train.nii")
mask = nibabel.load("sim1mask.nii")
mask = nibabel.Nifti1Image(mask.get_data(),
                           mask.get_affine())
a = np.array([1, 2])
y_train = a.repeat(30)
decoder = SpaceNetClassifier(loss='logistic', screening_percentile=100,
                             mask=mask).fit(X_train, y_train)


```

I'll look at it asap.

OK, the bug concerns the (internal) interpretation of `screening_percentile`: does 100% mean 100% of standard brain or 100% of **this** brain ? There was once a debate on this issue, and we retained: the first meaning.

For @Leo-YF-Liu's particular example, 100% of standard brain = 0.044438533900526642% of **this** brain, leading to an essentially empty mask.

I think it's more natural to let `screening_percentile=100` mean 100% of **this** brain, i.e full-brain. Users can more easily make sense out of this.

@dohmatob I tried to remove the option, i.e. decoder = SpaceNetClassifier(penalty=penalty1,loss = 'logistic',mask=mask). The same error still occur. What I want is just a mask that contains all the voxel in the image.

My data is not a typical brain image. It's just a simulated image of size  20 by 20 by 10 .

On Thu, Nov 5, 2015 at 5:19 PM, Leo-YF-Liu notifications@github.com wrote:

> My data is not a typical brain image. It's just a simulated 20 by 20 by 10
> image.
> 
> Ya, saw that already.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/nilearn/nilearn/issues/822#issuecomment-154109968.

## 

DED

OK, I just pushed a fix on PR #823.

Demo code.

```
import numpy as np
import matplotlib.pyplot as plt
import nibabel
from nilearn.decoding import SpaceNetClassifier
from nilearn.plotting import plot_stat_map
X_train = nibabel.load("sim2train.nii")
mask = nibabel.load("sim1mask.nii")
mask = nibabel.Nifti1Image(mask.get_data(),
                           mask.get_affine())
a = np.array([1, 2])
y_train = a.repeat(30)
for penalty in ['graph-net', 'tv-l1']:
    decoder = SpaceNetClassifier(
        penalty=penalty, loss='logistic', screening_percentile=100,
        verbose=2).fit(X_train, y_train)
    plot_stat_map(decoder.coef_img_, None, black_bg=True,
                  title=penalty.upper(), colorbar=True)
    plt.savefig("%s.png" % penalty)
plt.show()

```

![graph-net](https://cloud.githubusercontent.com/assets/634068/10974773/030df910-83e4-11e5-9ea6-f10ad46643ef.png)
![tv-l1](https://cloud.githubusercontent.com/assets/634068/10974779/06c9e208-83e4-11e5-97d4-8cb32aa550c5.png)

@dohmatob : I don't see anything.

But I am on it, I have diagnosed the problem, and I am adding logic (including warnings) to prevent from it.

@dohmatob Your results are pretty good, but I tried your demo code, the same error showed up again. Did you update any internal code in the nilearn package? Shall I wait for the update of the package before I run the code?

> Shall I wait for the update of the package before I run the code?

Yes, @domathob and myself are on it. Give me 20mn to do a PR, and another
hour of two for reviews and merge to happen.

Sorry, the PR link i gave you was wrong. It's here: https://github.com/nilearn/nilearn/pull/824.

Also, i didn't notice @GaelVaroquaux was working on the issue. So, you may ignore the above PR.


Resolving patch:
diff --git a/nilearn/decoding/space_net.py b/nilearn/decoding/space_net.py
--- a/nilearn/decoding/space_net.py
+++ b/nilearn/decoding/space_net.py
@@ -56,6 +56,40 @@ def _get_mask_volume(mask_img):
     return 1. * np.prod(vox_dims) * mask_img.get_data().astype(np.bool).sum()
 
 
+def _adjust_screening_percentile(screening_percentile, mask_img,
+                                 verbose=0):
+    original_screening_percentile = screening_percentile
+    # correct screening_percentile according to the volume of the data mask
+    mask_volume = _get_mask_volume(mask_img)
+    if mask_volume > MNI152_BRAIN_VOLUME:
+        warnings.warn(
+            "Brain mask is bigger than the volume of a standard "
+            "humain brain. SpaceNet is probably not tuned to"
+            "be used on such data.", stacklevel=2)
+    elif mask_volume < .005 * MNI152_BRAIN_VOLUME:
+        warnings.warn(
+            "Brain mask is smaller than .5% of the volume "
+            "humain brain. SpaceNet is probably not tuned to"
+            "be used on such data.", stacklevel=2)
+
+    if screening_percentile < 100:
+        screening_percentile = screening_percentile * (
+            MNI152_BRAIN_VOLUME / mask_volume)
+        screening_percentile = min(screening_percentile, 100)
+    # if screening_percentile is 100, we don't do anything
+
+    if verbose > 1:
+        print("Mask volume = %gmm^3 = %gcm^3" % (
+            mask_volume, mask_volume / 1.e3))
+        print("Standard brain volume = %gmm^3 = %gcm^3" % (
+            MNI152_BRAIN_VOLUME, MNI152_BRAIN_VOLUME / 1.e3))
+        print("Original screening-percentile: %g" % (
+            original_screening_percentile))
+        print("Volume-corrected screening-percentile: %g" % (
+            screening_percentile))
+    return screening_percentile
+
+
 def _crop_mask(mask):
     """Crops input mask to produce tighter (i.e smaller) bounding box with
     the same support (active voxels)"""
@@ -533,7 +567,8 @@ class BaseSpaceNet(LinearModel, RegressorMixin, CacheMixin):
         100 means 'keep all features'. This percentile is is expressed
         w.r.t the volume of a standard (MNI152) brain, and so is corrected
         at runtime to correspond to the volume of the user-supplied mask
-        (which is typically smaller).
+        (which is typically smaller). If '100' is given, all the features
+        are used, regardless of the number of voxels.
 
     standardize : bool, optional (default True):
         If set, then the data (X, y) are centered to have mean zero along
@@ -802,22 +837,9 @@ def fit(self, X, y):
         w = np.zeros((n_problems, X.shape[1] + 1))
         self.all_coef_ = np.ndarray((n_problems, n_folds, X.shape[1]))
 
-        # correct screening_percentile according to the volume of the data mask
-        mask_volume = _get_mask_volume(self.mask_img_)
-        if mask_volume > MNI152_BRAIN_VOLUME:
-            warnings.warn(
-                "Brain mask is bigger than volume of standard brain!")
-        self.screening_percentile_ = self.screening_percentile * (
-            mask_volume / MNI152_BRAIN_VOLUME)
-        if self.verbose > 1:
-            print("Mask volume = %gmm^3 = %gcm^3" % (
-                mask_volume, mask_volume / 1.e3))
-            print("Standard brain volume = %gmm^3 = %gcm^3" % (
-                MNI152_BRAIN_VOLUME, MNI152_BRAIN_VOLUME / 1.e3))
-            print("Original screening-percentile: %g" % (
-                self.screening_percentile))
-            print("Volume-corrected screening-percentile: %g" % (
-                self.screening_percentile_))
+        self.screening_percentile_ = _adjust_screening_percentile(
+                self.screening_percentile, self.mask_img_,
+                verbose=self.verbose)
 
         # main loop: loop on classes and folds
         solver_params = dict(tol=self.tol, max_iter=self.max_iter)
@@ -832,7 +854,7 @@ def fit(self, X, y):
                 solver_params, n_alphas=self.n_alphas, eps=self.eps,
                 is_classif=self.loss == "logistic", key=(cls, fold),
                 debias=self.debias, verbose=self.verbose,
-                screening_percentile=self.screening_percentile_
+                screening_percentile=self.screening_percentile_,
                 ) for cls in range(n_problems) for fold in range(n_folds)):
             self.best_model_params_.append((best_alpha, best_l1_ratio))
             self.alpha_grids_.append(alphas)
@@ -1007,7 +1029,8 @@ class SpaceNetClassifier(BaseSpaceNet):
         100 means 'keep all features'. This percentile is is expressed
         w.r.t the volume of a standard (MNI152) brain, and so is corrected
         at runtime by premultiplying it with the ratio of the volume of the
-        mask of the data and volume of a standard brain.
+        mask of the data and volume of a standard brain.  If '100' is given,
+        all the features are used, regardless of the number of voxels.
 
     standardize : bool, optional (default True):
         If set, then we'll center the data (X, y) have mean zero along axis 0.


Test patch:
diff --git a/nilearn/decoding/tests/test_space_net.py b/nilearn/decoding/tests/test_space_net.py
--- a/nilearn/decoding/tests/test_space_net.py
+++ b/nilearn/decoding/tests/test_space_net.py
@@ -12,11 +12,12 @@
 from sklearn.linear_model import Lasso
 from sklearn.utils import check_random_state
 from sklearn.linear_model import LogisticRegression
-from nilearn._utils.testing import assert_raises_regex
+from nilearn._utils.testing import assert_raises_regex, assert_warns
 from nilearn.decoding.space_net import (
     _EarlyStoppingCallback, _space_net_alpha_grid, MNI152_BRAIN_VOLUME,
     path_scores, BaseSpaceNet, _crop_mask, _univariate_feature_screening,
-    _get_mask_volume, SpaceNetClassifier, SpaceNetRegressor, _crop_mask)
+    _get_mask_volume, SpaceNetClassifier, SpaceNetRegressor,
+    _adjust_screening_percentile)
 from nilearn.decoding.space_net_solvers import (_graph_net_logistic,
                                                 _graph_net_squared_loss)
 
@@ -109,7 +110,16 @@ def test_params_correctly_propagated_in_constructors():
         assert_equal(cvobj.screening_percentile, perc)
 
 
-def testlogistic_path_scores():
+def test_screening_space_net():
+    screening_percentile = assert_warns(UserWarning,
+        _adjust_screening_percentile, 10, mask)
+    # We gave here a very small mask, judging by standards of brain size
+    # thus the screening_percentile_ corrected for brain size should
+    # be 100%
+    assert_equal(screening_percentile, 100)
+
+
+def test_logistic_path_scores():
     iris = load_iris()
     X, y = iris.data, iris.target
     _, mask = to_niimgs(X, [2, 2, 2])