Coverage for nilearn/decoding/decoder.py: 13%
400 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1"""High-level decoding object.
3Exposes standard classification and
4regression strategies such as SVM, LogisticRegression and Ridge,
5with optional feature selection,
6integrated hyper-parameter selection and aggregation
7strategy in which the best models within a cross validation loop are averaged.
9Also exposes a high-level method FREM that uses clustering and model
10ensembling to achieve state of the art performance
11"""
13import itertools
14import warnings
15from collections.abc import Iterable
17import numpy as np
18from joblib import Parallel, delayed
19from sklearn import clone
20from sklearn.base import (
21 BaseEstimator,
22 ClassifierMixin,
23 MultiOutputMixin,
24 RegressorMixin,
25)
26from sklearn.dummy import DummyClassifier, DummyRegressor
27from sklearn.linear_model import (
28 LassoCV,
29 LogisticRegressionCV,
30 RidgeClassifierCV,
31 RidgeCV,
32)
33from sklearn.metrics import check_scoring, get_scorer
34from sklearn.model_selection import (
35 LeaveOneGroupOut,
36 ParameterGrid,
37 ShuffleSplit,
38 StratifiedShuffleSplit,
39 check_cv,
40)
41from sklearn.preprocessing import LabelBinarizer
42from sklearn.svm import SVR, LinearSVC, l1_min_c
43from sklearn.utils.extmath import safe_sparse_dot
44from sklearn.utils.validation import check_is_fitted, check_X_y
46from nilearn._utils import CacheMixin, fill_doc
47from nilearn._utils.cache_mixin import check_memory
48from nilearn._utils.logger import find_stack_level
49from nilearn._utils.masker_validation import (
50 check_compatibility_mask_and_images,
51 check_embedded_masker,
52)
53from nilearn._utils.param_validation import (
54 check_feature_screening,
55 check_params,
56)
57from nilearn._utils.tags import SKLEARN_LT_1_6
58from nilearn.maskers import SurfaceMasker
59from nilearn.regions.rena_clustering import ReNA
60from nilearn.surface import SurfaceImage
62SUPPORTED_ESTIMATORS = {
63 "svc_l1": LinearSVC(penalty="l1", dual=False, max_iter=10000),
64 "svc_l2": LinearSVC(penalty="l2", dual=True, max_iter=10000),
65 "svc": LinearSVC(penalty="l2", dual=True, max_iter=10000),
66 "logistic_l1": LogisticRegressionCV(penalty="l1", solver="liblinear"),
67 "logistic_l2": LogisticRegressionCV(penalty="l2", solver="liblinear"),
68 "logistic": LogisticRegressionCV(penalty="l2", solver="liblinear"),
69 "ridge_classifier": RidgeClassifierCV(),
70 "ridge_regressor": RidgeCV(),
71 "ridge": RidgeCV(),
72 "lasso": LassoCV(),
73 "lasso_regressor": LassoCV(),
74 "svr": SVR(kernel="linear", max_iter=10000),
75 "dummy_classifier": DummyClassifier(strategy="stratified", random_state=0),
76 "dummy_regressor": DummyRegressor(strategy="mean"),
77}
80@fill_doc
81def _check_param_grid(estimator, X, y, param_grid=None):
82 """Check param_grid and return sensible default if param_grid is None.
84 Parameters
85 ----------
86 estimator : str
87 The estimator to choose among:
88 %(classifier_options)s
89 %(regressor_options)s
91 X : list of Niimg-like objects
92 See :ref:`extracting_data`.
93 Data on which model is to be fitted. If this is a list,
94 the affine is considered the same for all.
96 y : array or list of shape (n_samples)
97 The dependent variable (age, sex, IQ, yes/no, etc.).
98 Target variable to predict. Must have exactly as many elements as
99 3D images in niimg.
101 param_grid : dict of str to sequence, or sequence of such. Default None
102 The parameter grid to explore, as a dictionary mapping estimator
103 parameters to sequences of allowed values.
105 An empty dict signifies default parameters.
107 A sequence of dicts signifies a sequence of grids to search, and is
108 useful to avoid exploring parameter combinations that make no sense
109 or have no effect. See scikit-learn documentation for more information.
111 For Dummy estimators, parameter grid defaults to empty as these
112 estimators do not have hyperparameters to grid search.
114 Returns
115 -------
116 param_grid : dict of str to sequence, or sequence of such. Sensible default
117 dict has size 1 for linear models.
119 """
120 if param_grid is None:
121 param_grid = _default_param_grid(estimator, X, y)
123 elif isinstance(estimator, LogisticRegressionCV):
124 param_grid = _replace_param_grid_key(param_grid, "C", "Cs")
125 param_grid = _wrap_param_grid(param_grid, "Cs")
127 elif isinstance(estimator, (RidgeCV, RidgeClassifierCV, LassoCV)):
128 param_grid = _wrap_param_grid(param_grid, "alphas")
130 return param_grid
133def _default_param_grid(estimator, X, y):
134 """Generate sensible default for param_grid.
136 Parameters
137 ----------
138 estimator : str
139 The estimator to choose among:
140 %(classifier_options)s
141 %(regressor_options)s
143 X : list of Niimg-like objects
144 See :ref:`extracting_data`.
145 Data on which model is to be fitted. If this is a list,
146 the affine is considered the same for all.
148 y : array or list of shape (n_samples)
149 The dependent variable (age, sex, IQ, yes/no, etc.).
150 Target variable to predict. Must have exactly as many elements as
151 3D images in niimg.
153 Returns
154 -------
155 param_grid : dict of str to sequence, or sequence of such. Sensible default
156 dict has size 1 for linear models.
157 """
158 param_grid = {}
160 # validate estimator
161 if isinstance(estimator, (DummyClassifier, DummyRegressor)):
162 if estimator.strategy == "constant":
163 message = (
164 "Dummy classification implemented only for strategies"
165 ' "most_frequent", "prior", "stratified"'
166 )
167 raise NotImplementedError(message)
168 elif not isinstance(
169 estimator,
170 (
171 LogisticRegressionCV,
172 LinearSVC,
173 RidgeCV,
174 RidgeClassifierCV,
175 SVR,
176 LassoCV,
177 ),
178 ):
179 raise ValueError(
180 "Invalid estimator. The supported estimators are:"
181 f" {list(SUPPORTED_ESTIMATORS.keys())}"
182 )
184 # use l1_min_c to get lower bound for estimators with L1 penalty
185 if hasattr(estimator, "penalty") and (estimator.penalty == "l1"):
186 # define loss function
187 if isinstance(estimator, LogisticRegressionCV):
188 loss = "log"
189 elif isinstance(estimator, LinearSVC):
190 loss = "squared_hinge"
192 min_c = l1_min_c(X, y, loss=loss)
194 # otherwise use 0.5 which will give param_grid["C"] = [1, 10, 100]
195 else:
196 min_c = 0.5
198 # define sensible default for different types of estimators
199 if isinstance(estimator, (RidgeCV, RidgeClassifierCV)):
200 param_grid["alphas"] = [np.geomspace(1e-3, 1e4, 8)]
201 elif isinstance(estimator, LogisticRegressionCV):
202 # min_c value is set to 0.5 unless the estimator uses L1 penalty,
203 # in which case min_c is computed with sklearn.svm.l1_min_c(),
204 # so for L2 penalty, param_grid["Cs"] is either 1e-3, ..., 1e4, and
205 # for L1 penalty the values are obtained in a more data-driven way
206 param_grid["Cs"] = [np.geomspace(2e-3, 2e4, 8) * min_c]
207 elif isinstance(estimator, LassoCV):
208 # the default is to generate 30 alphas based on the data
209 # (alpha values can also be set with the 'alphas' parameter, in which
210 # case 'n_alphas' is ignored)
211 param_grid["n_alphas"] = [30]
212 elif isinstance(estimator, (LinearSVC, SVR)):
213 # similar logic as above:
214 # - for L2 penalty this is [1, 10, 100]
215 # - for L1 penalty the values depend on the data
216 param_grid["C"] = np.array([2, 20, 200]) * min_c
217 else:
218 param_grid = {}
220 return param_grid
223def _wrap_param_grid(param_grid, param_name):
224 """Wrap a parameter's sequence of values with an outer list.
226 This can be desirable for models featuring built-in cross-validation,
227 as it would leave it to the model's internal (optimized) cross-validation
228 to loop over hyperparameter values. Does nothing if the parameter is
229 already wrapped.
231 Parameters
232 ----------
233 param_grid : dict of str to sequence, or sequence of such
234 The parameter grid to wrap, as a dictionary mapping estimator
235 parameters to sequences of allowed values.
236 param_name : str
237 Name of parameter whose sequence of values should be wrapped
239 Returns
240 -------
241 dict of str to sequence, or sequence of such
242 The updated parameter grid
243 """
244 if param_grid is None:
245 return param_grid
247 # param_grid can be either a dict or a sequence of dicts
248 # we make sure that it is a sequence we can loop over
249 input_is_dict = isinstance(param_grid, dict)
250 if input_is_dict:
251 param_grid = [param_grid]
253 # process dicts one by one and add them to a new list
254 new_param_grid = []
255 for param_grid_item in param_grid:
256 if param_name in param_grid_item and not isinstance(
257 param_grid_item[param_name][0], Iterable
258 ):
259 warnings.warn(
260 f"parameter '{param_name}' should be a sequence of iterables"
261 f" (e.g., {{param_name: [[1, 10, 100]]}}) to benefit from"
262 " the built-in cross-validation of the estimator."
263 f" Wrapping {param_grid_item[param_name]} in an outer list.",
264 stacklevel=find_stack_level(),
265 )
267 param_grid_item = dict(param_grid_item) # make a new dict
268 param_grid_item[param_name] = [param_grid_item[param_name]]
270 new_param_grid.append(param_grid_item)
272 # return a dict (not a list) if the original input was a dict
273 if input_is_dict:
274 new_param_grid = new_param_grid[0]
276 return new_param_grid
279def _replace_param_grid_key(param_grid, key_to_replace, new_key):
280 """Replace a parameter name by another one.
282 Parameters
283 ----------
284 param_grid : dict of str to sequence, or sequence of such
285 The parameter grid to process, as a dictionary mapping estimator
286 parameters to sequences of allowed values.
287 key_to_replace : str
288 Name of parameter to replace
289 new_key : str
290 New parameter name. If this key already exists in the parameter grid,
291 it is overwritten
293 Returns
294 -------
295 dict of str to sequence, or sequence of such
296 The updated parameter grid
297 """
298 # ensure param_grid is a list so that we can loop over it
299 input_is_dict = isinstance(param_grid, dict)
300 if input_is_dict:
301 param_grid = [param_grid]
303 # replace old key by new key if needed
304 new_param_grid = []
305 for param_grid_item in param_grid:
306 param_grid_item = dict(param_grid_item) # make a new dict
307 if key_to_replace in param_grid_item:
308 warnings.warn(
309 f'The "{key_to_replace}" parameter in "param_grid" is'
310 f' being replaced by "{new_key}" due to a change in the'
311 " choice of underlying scikit-learn estimator. In a future"
312 " version, this will result in an error.",
313 DeprecationWarning,
314 stacklevel=find_stack_level(),
315 )
316 param_grid_item[new_key] = param_grid_item.pop(key_to_replace)
317 new_param_grid.append(param_grid_item)
319 # return a dict if input was a dict
320 if input_is_dict:
321 new_param_grid = new_param_grid[0]
323 return new_param_grid
326def _check_estimator(estimator):
327 if not isinstance(estimator, str):
328 warnings.warn(
329 "Use a custom estimator at your own risk "
330 "of the process not working as intended.",
331 stacklevel=find_stack_level(),
332 )
333 elif estimator in SUPPORTED_ESTIMATORS:
334 estimator = SUPPORTED_ESTIMATORS.get(estimator)
335 else:
336 raise ValueError(
337 "Invalid estimator. Known estimators are: "
338 f"{list(SUPPORTED_ESTIMATORS.keys())}"
339 )
341 return estimator
344def _parallel_fit(
345 estimator,
346 X,
347 y,
348 train,
349 test,
350 param_grid,
351 selector,
352 scorer,
353 mask_img,
354 class_index,
355 clustering_percentile,
356):
357 """Find the best estimator for a fold within a job.
359 This function tries several parameters for the estimator for the train and
360 test fold provided and save the one that performs best.
362 Fit may be performed after some preprocessing step :
363 * clustering with ReNA if clustering_percentile < 100
364 * feature screening if screening_percentile < 100
365 """
366 X_train, y_train = X[train], y[train]
367 X_test, y_test = X[test], y[test]
369 # for FREM Classifier and Regressor : start by doing a quick ReNA
370 # clustering to reduce the number of feature by agglomerating similar ones
372 if clustering_percentile < 100:
373 n_clusters = int(X_train.shape[1] * clustering_percentile / 100.0)
374 clustering = ReNA(
375 mask_img,
376 n_clusters=n_clusters,
377 n_iter=20,
378 threshold=1e-7,
379 scaling=False,
380 )
381 X_train = clustering.fit_transform(X_train)
382 X_test = clustering.transform(X_test)
384 do_screening = (X_train.shape[1] > 100) and selector is not None
386 if do_screening:
387 X_train = selector.fit_transform(X_train, y_train)
388 X_test = selector.transform(X_test)
390 # If there is no parameter grid, then we use a suitable grid (by default)
391 param_grid = ParameterGrid(
392 _check_param_grid(estimator, X_train, y_train, param_grid)
393 )
395 # collect all parameter names from the grid
396 all_params = set()
397 for params in param_grid:
398 all_params.update(params.keys())
400 best_score = None
401 for params in param_grid:
402 estimator = clone(estimator).set_params(**params)
403 estimator.fit(X_train, y_train)
405 score = scorer(estimator, X_test, y_test)
407 # Store best parameters and estimator coefficients
408 if (best_score is None) or (score >= best_score):
409 best_score = score
410 if hasattr(estimator, "coef_"):
411 best_coef = np.reshape(estimator.coef_, (1, -1))
412 best_intercept = estimator.intercept_
413 dummy_output = None
414 else:
415 best_coef, best_intercept = None, None
416 if isinstance(estimator, DummyClassifier):
417 dummy_output = estimator.class_prior_
418 elif isinstance(estimator, DummyRegressor):
419 dummy_output = estimator.constant_
421 if isinstance(estimator, (RidgeCV, RidgeClassifierCV, LassoCV)):
422 params["best_alpha"] = estimator.alpha_
423 elif isinstance(estimator, LogisticRegressionCV):
424 params["best_C"] = estimator.C_.item()
425 best_params = params
427 # fill in any missing param from param_grid
428 for param in all_params:
429 if param not in best_params:
430 best_params[param] = getattr(estimator, param)
432 if best_coef is not None:
433 if do_screening:
434 best_coef = selector.inverse_transform(best_coef)
436 if clustering_percentile < 100:
437 best_coef = clustering.inverse_transform(best_coef)
439 return (
440 class_index,
441 best_coef,
442 best_intercept,
443 best_params,
444 best_score,
445 dummy_output,
446 )
449@fill_doc
450class _BaseDecoder(CacheMixin, BaseEstimator):
451 """A wrapper for popular classification/regression strategies in \
452 neuroimaging.
454 The `BaseDecoder` object supports classification and regression methods.
455 It implements a model selection scheme that averages the best models
456 within a cross validation loop (a technique sometimes known as CV bagging).
457 The resulting average model is the one used as a classifier or a regressor.
458 This object also leverages the `NiftiMaskers` to provide a direct interface
459 with the Nifti files on disk.
461 Parameters
462 ----------
463 estimator : str, default='svc'
464 The estimator to use. For classification, choose among:
465 %(classifier_options)s
466 For regression, choose among:
467 %(regressor_options)s
469 mask : filename, Nifti1Image, NiftiMasker, MultiNiftiMasker, or\
470 SurfaceMasker, default=None
471 Mask to be used on data. If an instance of masker is passed,
472 then its mask and parameters will be used. If no mask is given, mask
473 will be computed automatically from provided images by an inbuilt
474 masker with default parameters. Refer to NiftiMasker, MultiNiftiMasker
475 or SurfaceMasker to check for default parameters. For use with
476 SurfaceImage data, a SurfaceMasker instance must be passed.
478 cv : cross-validation generator or int, default=10
479 A cross-validation generator.
480 See: https://scikit-learn.org/stable/modules/cross_validation.html
482 param_grid : dict of str to sequence, or sequence of such, default=None
483 The parameter grid to explore, as a dictionary mapping estimator
484 parameters to sequences of allowed values.
486 None or an empty dict signifies default parameters.
488 A sequence of dicts signifies a sequence of grids to search, and is
489 useful to avoid exploring parameter combinations that make no sense
490 or have no effect. See scikit-learn documentation for more information,
491 for example: https://scikit-learn.org/stable/modules/grid_search.html
493 For Dummy estimators, parameter grid defaults to empty dictionary.
495 clustering_percentile : int, float, in the [0, 100], default=100
496 Percentile of features to keep after clustering. If it is lower
497 than 100, a ReNA clustering is performed as a first step of fit
498 to agglomerate similar features together. ReNA is typically efficient
499 for clustering_percentile equal to 10. Only used with
500 :class:`nilearn.decoding.FREMClassifier` and
501 :class:`nilearn.decoding.FREMRegressor`.
503 screening_percentile : int, float, \
504 in the closed interval [0, 100], \
505 default=20
506 The percentage of brain volume that will be kept with respect to a full
507 MNI template. In particular, if it is lower than 100, a univariate
508 feature selection based on the Anova F-value for the input data will be
509 performed. A float according to a percentile of the highest
510 scores. If None is passed, the percentile is set to 100.
512 scoring : str, callable or None,
513 default=None
514 The scoring strategy to use. See the scikit-learn documentation at
515 https://scikit-learn.org/stable/modules/model_evaluation.html#the-scoring-parameter-defining-model-evaluation-rules
516 If callable, takes as arguments the fitted estimator, the
517 test data (X_test) and the test target (y_test) if y is
518 not None.
519 e.g. scorer(estimator, X_test, y_test)
521 For regression, valid entries are: 'r2', 'neg_mean_absolute_error', or
522 'neg_mean_squared_error'. Defaults to 'r2'.
524 For classification, valid entries are: 'accuracy', 'f1', 'precision',
525 'recall' or 'roc_auc'. Defaults to 'roc_auc'.
527 %(smoothing_fwhm)s
529 %(standardize)s
531 %(target_affine)s
533 %(target_shape)s
535 %(low_pass)s
537 %(high_pass)s
539 %(t_r)s
541 %(mask_strategy)s
543 .. note::
544 This parameter will be ignored if a mask image is provided.
546 .. note::
547 Depending on this value, the mask will be computed from
548 :func:`nilearn.masking.compute_background_mask`,
549 :func:`nilearn.masking.compute_epi_mask`, or
550 :func:`nilearn.masking.compute_brain_mask`.
552 Default is 'background'.
554 %(memory)s
556 %(memory_level)s
558 %(n_jobs)s
560 %(verbose0)s
562 See Also
563 --------
564 nilearn.decoding.Decoder: Classification strategies for Neuroimaging,
565 nilearn.decoding.DecoderRegressor: Regression strategies for Neuroimaging,
566 nilearn.decoding.FREMClassifier: State of the art classification pipeline
567 for Neuroimaging
568 nilearn.decoding.FREMRegressor: State of the art regression pipeline
569 for Neuroimaging
570 nilearn.decoding.SpaceNetClassifier: Graph-Net and TV-L1 priors/penalties
572 """
574 def __init__(
575 self,
576 estimator="svc",
577 mask=None,
578 cv=10,
579 param_grid=None,
580 clustering_percentile=100,
581 screening_percentile=20,
582 scoring=None,
583 smoothing_fwhm=None,
584 standardize=True,
585 target_affine=None,
586 target_shape=None,
587 low_pass=None,
588 high_pass=None,
589 t_r=None,
590 mask_strategy="background",
591 is_classification=True,
592 memory=None,
593 memory_level=0,
594 n_jobs=1,
595 verbose=0,
596 ):
597 self.estimator = estimator
598 self.mask = mask
599 self.cv = cv
600 self.param_grid = param_grid
601 self.screening_percentile = screening_percentile
602 self.scoring = scoring
603 self.is_classification = is_classification
604 self.clustering_percentile = clustering_percentile
605 self.smoothing_fwhm = smoothing_fwhm
606 self.standardize = standardize
607 self.target_affine = target_affine
608 self.target_shape = target_shape
609 self.mask_strategy = mask_strategy
610 self.low_pass = low_pass
611 self.high_pass = high_pass
612 self.t_r = t_r
613 self.memory = memory
614 self.memory_level = memory_level
615 self.n_jobs = n_jobs
616 self.verbose = verbose
618 @fill_doc
619 def fit(self, X, y, groups=None):
620 """Fit the decoder (learner).
622 Parameters
623 ----------
624 X : list of Niimg-like or :obj:`~nilearn.surface.SurfaceImage` objects
625 See :ref:`extracting_data`.
626 Data on which model is to be fitted. If this is a list,
627 the affine is considered the same for all.
629 y : numpy.ndarray of shape=(n_samples) or list of length n_samples
630 The dependent variable (age, sex, IQ, yes/no, etc.).
631 Target variable to predict. Must have exactly as many elements as
632 3D images in niimg.
634 %(groups)s
636 %(base_decoder_fit_attributes)s
638 """
639 check_params(self.__dict__)
640 self.estimator_ = _check_estimator(self.estimator)
641 self.memory_ = check_memory(self.memory, self.verbose)
643 X = self._apply_mask(X)
644 X, y = check_X_y(X, y, dtype=np.float64, multi_output=True)
646 self.n_outputs_ = 1 if y.ndim == 1 else y.shape[1]
648 self._set_scorer()
650 # Setup cross-validation object. Default is StratifiedKFold when groups
651 # is None. If groups is specified but self.cv is not set to custom CV
652 # splitter, default is LeaveOneGroupOut. If self.cv is manually set to
653 # a CV splitter object do check_cv regardless of groups parameter.
654 cv = self.cv
656 if isinstance(cv, int) and isinstance(self, FREMClassifier):
657 cv_object = StratifiedShuffleSplit(cv, random_state=0)
659 elif isinstance(cv, int) and isinstance(self, FREMRegressor):
660 cv_object = ShuffleSplit(cv, random_state=0)
662 elif (isinstance(cv, int) or cv is None) and groups is not None:
663 warnings.warn(
664 "groups parameter is specified but "
665 "cv parameter is not set to custom CV splitter. "
666 "Using default object LeaveOneGroupOut().",
667 stacklevel=find_stack_level(),
668 )
669 cv_object = LeaveOneGroupOut()
671 else:
672 cv_object = check_cv(cv, y=y, classifier=self.is_classification)
674 self.cv_ = list(cv_object.split(X, y, groups=groups))
676 # Define the number problems to solve. In case of classification this
677 # number corresponds to the number of binary problems to solve
678 y = self._binarize_y(y) if self.is_classification else y[:, np.newaxis]
680 if self.is_classification and self.n_classes_ > 2:
681 n_problems = self.n_classes_
682 else:
683 n_problems = 1
685 # Check if the size of the mask image and the number of features allow
686 # to perform feature screening.
687 # If the input data is a SurfaceImage, the number of vertices in the
688 # mesh is needed to perform feature screening.
689 mesh_n_vertices = (
690 self.mask_img_.mesh.n_vertices
691 if isinstance(self.mask_img_, SurfaceImage)
692 else None
693 )
694 selector = check_feature_screening(
695 self.screening_percentile,
696 self.mask_img_,
697 self.is_classification,
698 mesh_n_vertices=mesh_n_vertices,
699 )
701 # Return a suitable screening percentile according to the mask image
702 if hasattr(selector, "percentile"):
703 self.screening_percentile_ = selector.percentile
704 elif self.screening_percentile is None:
705 self.screening_percentile_ = 100.0
706 else:
707 self.screening_percentile_ = self.screening_percentile
709 n_final_features = int(
710 X.shape[1]
711 * self.screening_percentile_
712 * self.clustering_percentile
713 / 10000
714 )
715 if n_final_features < 50:
716 warnings.warn(
717 "After clustering and screening, the decoding model will "
718 f"be trained only on {n_final_features} features. "
719 "Consider raising clustering_percentile or "
720 "screening_percentile parameters.",
721 UserWarning,
722 stacklevel=find_stack_level(),
723 )
725 parallel = Parallel(n_jobs=self.n_jobs, verbose=2 * self.verbose)
727 parallel_fit_outputs = parallel(
728 delayed(self._cache(_parallel_fit))(
729 estimator=self.estimator_,
730 X=X,
731 y=y[:, c],
732 train=train,
733 test=test,
734 param_grid=self.param_grid,
735 selector=selector,
736 scorer=self.scorer_,
737 mask_img=self.mask_img_,
738 class_index=c,
739 clustering_percentile=self.clustering_percentile,
740 )
741 for c, (train, test) in itertools.product(
742 range(n_problems), self.cv_
743 )
744 )
746 coefs, intercepts = self._fetch_parallel_fit_outputs(
747 parallel_fit_outputs, y, n_problems
748 )
750 # Build the final model (the aggregated one)
751 if not isinstance(self.estimator_, (DummyClassifier, DummyRegressor)):
752 self.coef_ = np.vstack(
753 [
754 np.mean(coefs[class_index], axis=0)
755 for class_index in self.classes_
756 ]
757 )
758 self.std_coef_ = np.vstack(
759 [
760 np.std(coefs[class_index], axis=0)
761 for class_index in self.classes_
762 ]
763 )
764 self.intercept_ = np.hstack(
765 [
766 np.mean(intercepts[class_index], axis=0)
767 for class_index in self.classes_
768 ]
769 )
771 self.coef_img_, self.std_coef_img_ = self._output_image(
772 self.classes_, self.coef_, self.std_coef_
773 )
775 if self.is_classification and (self.n_classes_ == 2):
776 self.coef_ = self.coef_[0, :][np.newaxis, :]
777 self.intercept_ = self.intercept_[0]
778 else:
779 # For Dummy estimators
780 self.coef_ = None
781 self.dummy_output_ = np.vstack(
782 [
783 np.mean(self.dummy_output_[class_index], axis=0)
784 for class_index in self.classes_
785 ]
786 )
787 if self.is_classification and (self.n_classes_ == 2):
788 self.dummy_output_ = self.dummy_output_[0, :][np.newaxis, :]
790 return self
792 def __sklearn_is_fitted__(self):
793 return hasattr(self, "coef_") and hasattr(self, "masker_")
795 def score(self, X, y, *args):
796 """Compute the prediction score using the scoring \
797 metric defined by the scoring attribute.
799 Parameters
800 ----------
801 X : Niimg-like, :obj:`list` of either \
802 Niimg-like objects or :obj:`str` or path-like
803 See :ref:`extracting_data`.
804 Data on which prediction is to be made.
806 y : :class:`numpy.ndarray`
807 Target values.
809 args : Optional arguments that can be passed to
810 scoring metrics. Example: sample_weight.
812 Returns
813 -------
814 score : float
815 Prediction score.
817 """
818 check_is_fitted(self)
819 return self.scorer_(self, X, y, *args)
821 def decision_function(self, X):
822 """Predict class labels for samples in X.
824 Parameters
825 ----------
826 X : Niimg-like, :obj:`list` of either \
827 Niimg-like objects or :obj:`str` or path-like
828 See :ref:`extracting_data`.
829 Data on prediction is to be made. If this is a list,
830 the affine is considered the same for all.
832 Returns
833 -------
834 y_pred : :class:`numpy.ndarray`, shape (n_samples,)
835 Predicted class label per sample.
836 """
837 check_is_fitted(self)
838 # for backwards compatibility - apply masker transform if X is
839 # niimg-like or a list of strings
840 if not isinstance(X, np.ndarray) or len(np.shape(X)) == 1:
841 X = self.masker_.transform(X)
842 n_features = self.coef_.shape[1]
843 if X.shape[1] != n_features:
844 raise ValueError(
845 f"X has {X.shape[1]} features per sample;"
846 f" expecting {n_features}"
847 )
849 scores = (
850 safe_sparse_dot(X, self.coef_.T, dense_output=True)
851 + self.intercept_
852 )
854 return scores.ravel() if scores.shape[1] == 1 else scores
856 def predict(self, X):
857 """Predict a label for all X vectors indexed by the first axis.
859 Parameters
860 ----------
861 X : Niimg-like, :obj:`list` of either \
862 Niimg-like objects or :obj:`str` or path-like
863 See :ref:`extracting_data`.
864 Data on which prediction is to be made.
866 Returns
867 -------
868 array, shape=(n_samples,) if n_classes == 2 else (n_samples, n_classes)
869 Confidence scores per (sample, class) combination. In the binary
870 case, confidence score for self.classes_[1] where >0 means this
871 class would be predicted.
872 """
873 check_is_fitted(self)
875 n_samples = np.shape(X)[-1]
877 # Prediction for dummy estimator is different from others as there is
878 # no fitted coefficient
879 if isinstance(self.estimator_, (DummyClassifier, DummyRegressor)):
880 scores = self._predict_dummy(n_samples)
881 else:
882 scores = self.decision_function(X)
884 if self.is_classification:
885 if scores.ndim == 1:
886 indices = (scores > 0).astype(int)
887 else:
888 indices = scores.argmax(axis=1)
889 return self.classes_[indices]
891 return scores
893 def _apply_mask(self, X):
894 masker_type = "nii"
895 # all elements of X should be of the similar type by now
896 # so we can only check the first one
897 to_check = X[0] if isinstance(X, Iterable) else X
898 if isinstance(self.mask, (SurfaceMasker, SurfaceImage)) or (
899 isinstance(to_check, SurfaceImage)
900 ):
901 masker_type = "surface"
903 self.masker_ = check_embedded_masker(self, masker_type=masker_type)
904 check_compatibility_mask_and_images(self.mask, X)
906 X = self.masker_.fit_transform(X)
907 self.mask_img_ = self.masker_.mask_img_
909 return X
911 def _fetch_parallel_fit_outputs(
912 self,
913 parallel_fit_outputs,
914 y, # noqa: ARG002
915 n_problems,
916 ):
917 """Fetch the outputs from parallel_fit to be ready for ensembling.
919 Parameters
920 ----------
921 parallel_fit_outputs : list of tuples,
922 each tuple contains results of
923 one _parallel_fit for each cv fold (and each classification in the
924 case of multiclass classification).
926 y : ndarray, shape = (n_samples, )
927 Vector of responses.
929 Returns
930 -------
931 coefs : dict
932 Coefficients for each classification/regression problem
933 intercepts : dict
934 Intercept for each classification/regression problem
935 """
936 coefs = {}
937 intercepts = {}
938 cv_scores = {}
939 self.cv_params_ = {}
940 self.dummy_output_ = {}
941 classes = self.classes_
943 for (
944 class_index,
945 coef,
946 intercept,
947 params,
948 scores,
949 dummy_output,
950 ) in parallel_fit_outputs:
951 coefs.setdefault(classes[class_index], []).append(coef)
952 intercepts.setdefault(classes[class_index], []).append(intercept)
954 cv_scores.setdefault(classes[class_index], []).append(scores)
956 self.cv_params_.setdefault(classes[class_index], {})
957 if isinstance(self.estimator_, (DummyClassifier, DummyRegressor)):
958 self.dummy_output_.setdefault(classes[class_index], []).append(
959 dummy_output
960 )
961 else:
962 self.dummy_output_.setdefault(classes[class_index], []).append(
963 None
964 )
965 for k in params:
966 self.cv_params_[classes[class_index]].setdefault(k, []).append(
967 params[k]
968 )
970 if (n_problems <= 2) and self.is_classification:
971 # Binary classification
972 other_class = np.setdiff1d(classes, classes[class_index])[0]
973 if coef is not None:
974 coefs.setdefault(other_class, []).append(-coef)
975 intercepts.setdefault(other_class, []).append(-intercept)
976 else:
977 coefs.setdefault(other_class, []).append(None)
978 intercepts.setdefault(other_class, []).append(None)
980 cv_scores.setdefault(other_class, []).append(scores)
981 self.cv_params_[other_class] = self.cv_params_[
982 classes[class_index]
983 ]
984 if isinstance(
985 self.estimator_, (DummyClassifier, DummyRegressor)
986 ):
987 self.dummy_output_.setdefault(other_class, []).append(
988 dummy_output
989 )
990 else:
991 self.dummy_output_.setdefault(other_class, []).append(None)
993 self.cv_scores_ = cv_scores
995 return coefs, intercepts
997 def _set_scorer(self):
998 if self.scoring is not None:
999 self.scorer_ = check_scoring(self.estimator_, self.scoring)
1000 elif self.is_classification:
1001 self.scorer_ = get_scorer("accuracy")
1002 else:
1003 self.scorer_ = get_scorer("r2")
1005 def _output_image(self, classes, coefs, std_coef):
1006 coef_img = {}
1007 std_coef_img = {}
1008 for class_index, coef, std in zip(classes, coefs, std_coef):
1009 coef_img[class_index] = self.masker_.inverse_transform(coef)
1010 std_coef_img[class_index] = self.masker_.inverse_transform(std)
1012 return coef_img, std_coef_img
1014 def _binarize_y(self, y):
1015 """Encode target classes as -1 and 1.
1017 Helper function invoked just before fitting a classifier.
1018 """
1019 y = np.array(y)
1021 self._enc = LabelBinarizer(pos_label=1, neg_label=-1)
1022 y = self._enc.fit_transform(y)
1023 self.classes_ = self._enc.classes_
1024 self.n_classes_ = len(self.classes_)
1025 return y
1027 def _predict_dummy(self, n_samples):
1028 """Non-sparse scikit-learn based prediction steps for classification \
1029 and regression.
1030 """
1031 if len(self.dummy_output_) == 1:
1032 dummy_output = self.dummy_output_[0]
1033 else:
1034 dummy_output = self.dummy_output_[:, 1]
1035 if isinstance(self.estimator_, DummyClassifier):
1036 strategy = self.estimator_.get_params()["strategy"]
1037 if strategy in ["most_frequent", "prior"]:
1038 scores = np.tile(dummy_output, reps=(n_samples, 1))
1039 elif strategy == "stratified":
1040 rs = np.random.default_rng(0)
1041 scores = rs.multinomial(1, dummy_output, size=n_samples)
1043 elif isinstance(self.estimator_, DummyRegressor):
1044 scores = np.full(
1045 (n_samples, self.n_outputs_),
1046 self.dummy_output_,
1047 dtype=np.array(self.dummy_output_).dtype,
1048 )
1049 return scores.ravel() if scores.shape[1] == 1 else scores
1051 def _more_tags(self):
1052 """Return estimator tags.
1054 TODO remove when bumping sklearn_version > 1.5
1055 """
1056 return self.__sklearn_tags__()
1058 def __sklearn_tags__(self):
1059 """Return estimator tags.
1061 See the sklearn documentation for more details on tags
1062 https://scikit-learn.org/1.6/developers/develop.html#estimator-tags
1063 """
1064 # TODO
1065 # get rid of if block
1066 # bumping sklearn_version > 1.5
1067 # see https://github.com/scikit-learn/scikit-learn/pull/29677
1068 if SKLEARN_LT_1_6:
1069 from nilearn._utils.tags import tags
1071 return tags(require_y=True, niimg_like=True, surf_img=True)
1073 from nilearn._utils.tags import InputTags
1075 tags = super().__sklearn_tags__()
1076 tags.target_tags.required = True
1077 tags.input_tags = InputTags(niimg_like=True, surf_img=True)
1078 return tags
1081@fill_doc
1082class Decoder(ClassifierMixin, _BaseDecoder):
1083 """A wrapper for popular classification strategies in neuroimaging.
1085 The `Decoder` object supports classification methods.
1086 It implements a model selection scheme that averages the best models
1087 within a cross validation loop. The resulting average model is the
1088 one used as a classifier. This object also leverages the`NiftiMaskers` to
1089 provide a direct interface with the Nifti files on disk.
1091 Parameters
1092 ----------
1093 estimator : :obj:`str`, default='svc'
1094 The estimator to choose among:
1095 %(classifier_options)s
1097 mask : filename, Nifti1Image, NiftiMasker, MultiNiftiMasker, \
1098 :obj:`~nilearn.surface.SurfaceImage` \
1099 or :obj:`~nilearn.maskers.SurfaceMasker`, default=None
1100 Mask to be used on data. If an instance of masker is passed,
1101 then its mask and parameters will be used. If no mask is given, mask
1102 will be computed automatically from provided images by an inbuilt
1103 masker with default parameters.
1104 Refer to :obj:`~nilearn.maskers.NiftiMasker` or
1105 :obj:`~nilearn.maskers.MultiNiftiMasker` or
1106 :obj:`~nilearn.maskers.SurfaceMasker`
1107 to check for default parameters.
1109 cv : cross-validation generator or :obj:`int`, default=10
1110 A cross-validation generator.
1111 See: https://scikit-learn.org/stable/modules/cross_validation.html.
1112 The default 10 refers to K = 10 folds of
1113 :class:`~sklearn.model_selection.StratifiedKFold` when groups is None
1114 in the fit method for this class. If groups is specified but ``cv``
1115 is not set to custom CV splitter, default is
1116 :class:`~sklearn.model_selection.LeaveOneGroupOut`.
1118 param_grid : :obj:`dict` of :obj:`str` to sequence, or sequence of such, \
1119 or None, default=None
1120 The parameter grid to explore, as a dictionary mapping estimator
1121 parameters to sequences of allowed values.
1123 None or an empty dict signifies default parameters.
1125 A sequence of dicts signifies a sequence of grids to search, and is
1126 useful to avoid exploring parameter combinations that make no sense
1127 or have no effect. See scikit-learn documentation for more information,
1128 for example: https://scikit-learn.org/stable/modules/grid_search.html
1130 For DummyClassifier, parameter grid defaults to empty dictionary, class
1131 predictions are estimated using default strategy.
1133 screening_percentile : :obj:`int`, :obj:`float`, optional, \
1134 in the closed interval [0, 100], default=20
1135 The percentage of brain volume that will be kept with respect to a full
1136 MNI template. In particular, if it is lower than 100, a univariate
1137 feature selection based on the Anova F-value for the input data will be
1138 performed. A float according to a percentile of the highest scores.
1140 scoring : :obj:`str`, callable or None, default='roc_auc'
1141 The scoring strategy to use. See the scikit-learn documentation at
1142 https://scikit-learn.org/stable/modules/model_evaluation.html#the-scoring-parameter-defining-model-evaluation-rules
1143 If callable, takes as arguments the fitted estimator, the
1144 test data (X_test) and the test target (y_test) if y is
1145 not None.
1146 e.g. scorer(estimator, X_test, y_test)
1148 For classification, valid entries are: 'accuracy', 'f1', 'precision',
1149 'recall' or 'roc_auc'.
1151 %(smoothing_fwhm)s
1153 %(standardize)s
1155 %(target_affine)s
1157 %(target_shape)s
1159 %(low_pass)s
1161 %(high_pass)s
1163 %(t_r)s
1165 %(mask_strategy)s
1167 .. note::
1168 This parameter will be ignored if a mask image is provided.
1170 .. note::
1171 Depending on this value, the mask will be computed from
1172 :func:`nilearn.masking.compute_background_mask`,
1173 :func:`nilearn.masking.compute_epi_mask`, or
1174 :func:`nilearn.masking.compute_brain_mask`.
1176 Default='background'.
1178 %(memory)s
1180 %(memory_level)s
1182 %(n_jobs)s
1184 %(verbose0)s
1186 See Also
1187 --------
1188 nilearn.decoding.DecoderRegressor: regression strategies for Neuro-imaging,
1189 nilearn.decoding.FREMClassifier: State of the art classification pipeline
1190 for Neuroimaging
1191 nilearn.decoding.SpaceNetClassifier: Graph-Net and TV-L1 priors/penalties
1192 """
1194 def __init__(
1195 self,
1196 estimator="svc",
1197 mask=None,
1198 cv=10,
1199 param_grid=None,
1200 screening_percentile=20,
1201 scoring="roc_auc",
1202 smoothing_fwhm=None,
1203 standardize=True,
1204 target_affine=None,
1205 target_shape=None,
1206 mask_strategy="background",
1207 low_pass=None,
1208 high_pass=None,
1209 t_r=None,
1210 memory=None,
1211 memory_level=0,
1212 n_jobs=1,
1213 verbose=0,
1214 ):
1215 super().__init__(
1216 estimator=estimator,
1217 mask=mask,
1218 cv=cv,
1219 param_grid=param_grid,
1220 screening_percentile=screening_percentile,
1221 scoring=scoring,
1222 smoothing_fwhm=smoothing_fwhm,
1223 standardize=standardize,
1224 target_affine=target_affine,
1225 target_shape=target_shape,
1226 mask_strategy=mask_strategy,
1227 low_pass=low_pass,
1228 high_pass=high_pass,
1229 t_r=t_r,
1230 memory=memory,
1231 is_classification=True,
1232 memory_level=memory_level,
1233 verbose=verbose,
1234 n_jobs=n_jobs,
1235 )
1236 # TODO remove for sklearn>=1.6
1237 self._estimator_type = "classifier"
1239 def _more_tags(self):
1240 """Return estimator tags.
1242 TODO remove when bumping sklearn_version > 1.5
1243 """
1244 return self.__sklearn_tags__()
1246 def __sklearn_tags__(self):
1247 """Return estimator tags.
1249 See the sklearn documentation for more details on tags
1250 https://scikit-learn.org/1.6/developers/develop.html#estimator-tags
1251 """
1252 # TODO
1253 # get rid of if block
1254 # bumping sklearn_version > 1.5
1255 # see https://github.com/scikit-learn/scikit-learn/pull/29677
1256 tags = super().__sklearn_tags__()
1257 if SKLEARN_LT_1_6:
1258 return tags
1260 from sklearn.utils import ClassifierTags
1262 tags.estimator_type = "classifier"
1263 tags.classifier_tags = ClassifierTags()
1265 return tags
1268@fill_doc
1269class DecoderRegressor(MultiOutputMixin, RegressorMixin, _BaseDecoder):
1270 """A wrapper for popular regression strategies in neuroimaging.
1272 The `DecoderRegressor` object supports regression methods.
1273 It implements a model selection scheme that averages the best models
1274 within a cross validation loop. The resulting average model is the
1275 one used as a regressor. This object also leverages the `NiftiMaskers`
1276 to provide a direct interface with the Nifti files on disk.
1278 Parameters
1279 ----------
1280 estimator : :obj:`str`, optional
1281 The estimator to choose among:
1282 %(regressor_options)s
1283 Default 'svr'.
1285 mask : filename, Nifti1Image, NiftiMasker, or MultiNiftiMasker, optional
1286 Mask to be used on data. If an instance of masker is passed,
1287 then its mask and parameters will be used. If no mask is given, mask
1288 will be computed automatically from provided images by an inbuilt
1289 masker with default parameters. Refer to NiftiMasker or
1290 MultiNiftiMasker to check for default parameters. Default None
1292 cv : cross-validation generator or :obj:`int`, default=10
1293 A cross-validation generator.
1294 See: https://scikit-learn.org/stable/modules/cross_validation.html.
1295 The default 10 refers to K = 10 folds of
1296 :class:`~sklearn.model_selection.StratifiedKFold` when groups is None
1297 in the fit method for this class. If groups is specified but ``cv``
1298 is not set to custom CV splitter, default is
1299 :class:`~sklearn.model_selection.LeaveOneGroupOut`.
1301 param_grid : :obj:`dict` of :obj:`str` to sequence, or sequence of such, \
1302 or None, default=None
1303 The parameter grid to explore, as a dictionary mapping estimator
1304 parameters to sequences of allowed values.
1306 None or an empty dict signifies default parameters.
1308 A sequence of dicts signifies a sequence of grids to search, and is
1309 useful to avoid exploring parameter combinations that make no sense
1310 or have no effect. See scikit-learn documentation for more information,
1311 for example: https://scikit-learn.org/stable/modules/grid_search.html
1313 For DummyRegressor, parameter grid defaults to empty dictionary, class
1314 predictions are estimated using default strategy.
1316 screening_percentile : :obj:`int`, :obj:`float`, \
1317 in the closed interval [0, 100], \
1318 default=20
1319 The percentage of brain volume that will be kept with respect to a full
1320 MNI template. In particular, if it is lower than 100, a univariate
1321 feature selection based on the Anova F-value for the input data will be
1322 performed. A float according to a percentile of the highest
1323 scores.
1325 scoring : :obj:`str`, callable or None, optional. default='r2'
1326 The scoring strategy to use. See the scikit-learn documentation at
1327 https://scikit-learn.org/stable/modules/model_evaluation.html#the-scoring-parameter-defining-model-evaluation-rules
1328 If callable, takes as arguments the fitted estimator, the
1329 test data (X_test) and the test target (y_test) if y is
1330 not None.
1331 e.g. scorer(estimator, X_test, y_test)
1333 For regression, valid entries are: 'r2', 'neg_mean_absolute_error',
1334 or 'neg_mean_squared_error'.
1336 %(smoothing_fwhm)s
1338 %(standardize)s
1340 %(target_affine)s
1342 %(target_shape)s
1344 %(low_pass)s
1346 %(high_pass)s
1348 %(t_r)s
1350 %(mask_strategy)s
1352 .. note::
1353 This parameter will be ignored if a mask image is provided.
1355 .. note::
1356 Depending on this value, the mask will be computed from
1357 :func:`nilearn.masking.compute_background_mask`,
1358 :func:`nilearn.masking.compute_epi_mask`, or
1359 :func:`nilearn.masking.compute_brain_mask`.
1361 Default='background'.
1363 %(memory)s
1365 %(memory_level)s
1367 %(n_jobs)s
1369 %(verbose0)s
1371 See Also
1372 --------
1373 nilearn.decoding.Decoder: classification strategies for Neuroimaging,
1374 nilearn.decoding.FREMRegressor: State of the art regression pipeline
1375 for Neuroimaging
1376 nilearn.decoding.SpaceNetClassifier: Graph-Net and TV-L1 priors/penalties
1377 """
1379 def __init__(
1380 self,
1381 estimator="svr",
1382 mask=None,
1383 cv=10,
1384 param_grid=None,
1385 screening_percentile=20,
1386 scoring="r2",
1387 smoothing_fwhm=None,
1388 standardize=True,
1389 target_affine=None,
1390 target_shape=None,
1391 mask_strategy="background",
1392 low_pass=None,
1393 high_pass=None,
1394 t_r=None,
1395 memory=None,
1396 memory_level=0,
1397 n_jobs=1,
1398 verbose=0,
1399 ):
1400 super().__init__(
1401 estimator=estimator,
1402 mask=mask,
1403 cv=cv,
1404 param_grid=param_grid,
1405 screening_percentile=screening_percentile,
1406 scoring=scoring,
1407 smoothing_fwhm=smoothing_fwhm,
1408 standardize=standardize,
1409 target_affine=target_affine,
1410 target_shape=target_shape,
1411 low_pass=low_pass,
1412 high_pass=high_pass,
1413 t_r=t_r,
1414 mask_strategy=mask_strategy,
1415 memory=memory,
1416 is_classification=False,
1417 memory_level=memory_level,
1418 verbose=verbose,
1419 n_jobs=n_jobs,
1420 )
1422 # TODO remove for sklearn>=1.6
1423 self._estimator_type = "regressor"
1425 def _more_tags(self):
1426 """Return estimator tags.
1428 TODO remove when bumping sklearn_version > 1.5
1429 """
1430 return self.__sklearn_tags__()
1432 def __sklearn_tags__(self):
1433 """Return estimator tags.
1435 See the sklearn documentation for more details on tags
1436 https://scikit-learn.org/1.6/developers/develop.html#estimator-tags
1437 """
1438 # TODO
1439 # get rid of if block
1440 # bumping sklearn_version > 1.5
1441 # see https://github.com/scikit-learn/scikit-learn/pull/29677
1442 tags = super().__sklearn_tags__()
1443 if SKLEARN_LT_1_6:
1444 tags["multioutput"] = True
1445 return tags
1446 from sklearn.utils import RegressorTags
1448 tags.estimator_type = "regressor"
1449 tags.regressor_tags = RegressorTags()
1451 return tags
1453 @fill_doc
1454 def fit(self, X, y, groups=None):
1455 """Fit the decoder (learner).
1457 Parameters
1458 ----------
1459 X : list of Niimg-like or :obj:`~nilearn.surface.SurfaceImage` objects
1460 See :ref:`extracting_data`.
1461 Data on which model is to be fitted. If this is a list,
1462 the affine is considered the same for all.
1464 y : numpy.ndarray of shape=(n_samples) or list of length n_samples
1465 The dependent variable (age, sex, IQ, yes/no, etc.).
1466 Target variable to predict. Must have exactly as many elements as
1467 3D images in niimg.
1469 %(groups)s
1471 %(base_decoder_fit_attributes)s
1473 """
1474 check_params(self.__dict__)
1475 self.classes_ = ["beta"]
1476 return super().fit(X, y, groups=groups)
1479@fill_doc
1480class FREMRegressor(_BaseDecoder):
1481 """State of the art :term:`decoding` scheme applied \
1482 to usual regression estimators.
1484 FREM uses an implicit spatial regularization through fast clustering and
1485 aggregates a high number of estimators trained on various splits of the
1486 training set, thus returning a very robust decoder
1487 at a lower computational cost
1488 than other spatially regularized methods :footcite:p:`Hoyos-Idrobo2018`.
1490 Parameters
1491 ----------
1492 estimator : :obj:`str`, optional
1493 The estimator to choose among:
1494 %(regressor_options)s
1495 Default 'svr'.
1497 mask : filename, Nifti1Image, NiftiMasker, or MultiNiftiMasker, \
1498 default=None
1499 Mask to be used on data. If an instance of masker is passed,
1500 then its mask and parameters will be used. If no mask is given, mask
1501 will be computed automatically from provided images by an inbuilt
1502 masker with default parameters. Refer to NiftiMasker or
1503 MultiNiftiMasker to check for default parameters.
1505 cv : :obj:`int` or cross-validation generator, default=30
1506 If int, number of shuffled splits returned, which is usually the right
1507 way to train many different classifiers. A good trade-off between
1508 stability of the aggregated model and computation time is 50 splits.
1509 Shuffled splits are seeded by default for reproducibility.
1510 Can also be a cross-validation generator.
1512 param_grid : :obj:`dict` of :obj:`str` to sequence, or sequence of such. \
1513 or None, default=None
1514 The parameter grid to explore, as a dictionary mapping estimator
1515 parameters to sequences of allowed values.
1517 None or an empty dict signifies default parameters.
1519 A sequence of dicts signifies a sequence of grids to search, and is
1520 useful to avoid exploring parameter combinations that make no sense
1521 or have no effect. See scikit-learn documentation for more information,
1522 for example: https://scikit-learn.org/stable/modules/grid_search.html
1524 clustering_percentile : :obj:`int`, :obj:`float`, \
1525 in closed interval [0, 100] \
1526 default=10
1527 Used to perform a fast ReNA clustering on input data as a first step of
1528 fit. It agglomerates similar features together to reduce their number
1529 by this percentile. ReNA is typically efficient for cluster_percentile
1530 equal to 10.
1532 screening_percentile : :obj:`int`, :obj:`float`, \
1533 in closed interval [0, 100] \
1534 default=20
1535 The percentage of brain volume that will be kept with respect to a full
1536 MNI template. In particular, if it is lower than 100, a univariate
1537 feature selection based on the Anova F-value for the input data will be
1538 performed. A float according to a percentile of the highest
1539 scores.
1541 scoring : :obj:`str`, callable or None, default= 'r2'
1543 The scoring strategy to use. See the scikit-learn documentation at
1544 https://scikit-learn.org/stable/modules/model_evaluation.html#the-scoring-parameter-defining-model-evaluation-rules
1545 If callable, takes as arguments the fitted estimator, the
1546 test data (X_test) and the test target (y_test) if y is
1547 not None.
1548 e.g. scorer(estimator, X_test, y_test)
1550 For regression, valid entries are: 'r2', 'neg_mean_absolute_error',
1551 or 'neg_mean_squared_error'.
1552 %(smoothing_fwhm)s
1553 %(standardize)s
1554 %(target_affine)s
1555 %(target_shape)s
1556 %(low_pass)s
1557 %(high_pass)s
1558 %(t_r)s
1559 %(mask_strategy)s
1561 .. note::
1562 This parameter will be ignored if a mask image is provided.
1564 .. note::
1565 Depending on this value, the mask will be computed from
1566 :func:`nilearn.masking.compute_background_mask`,
1567 :func:`nilearn.masking.compute_epi_mask`, or
1568 :func:`nilearn.masking.compute_brain_mask`.
1570 Default='background'.
1571 %(memory)s
1572 %(memory_level)s
1573 %(n_jobs)s
1574 %(verbose0)s
1576 References
1577 ----------
1578 .. footbibliography::
1580 See Also
1581 --------
1582 nilearn.decoding.DecoderRegressor: Regression strategies for Neuroimaging,
1583 nilearn.decoding.FREMClassifier: State of the art classification pipeline
1584 for Neuroimaging
1585 """
1587 def __init__(
1588 self,
1589 estimator="svr",
1590 mask=None,
1591 cv=30,
1592 param_grid=None,
1593 clustering_percentile=10,
1594 screening_percentile=20,
1595 scoring="r2",
1596 smoothing_fwhm=None,
1597 standardize=True,
1598 target_affine=None,
1599 target_shape=None,
1600 mask_strategy="background",
1601 low_pass=None,
1602 high_pass=None,
1603 t_r=None,
1604 memory=None,
1605 memory_level=0,
1606 n_jobs=1,
1607 verbose=0,
1608 ):
1609 super().__init__(
1610 estimator=estimator,
1611 mask=mask,
1612 cv=cv,
1613 param_grid=param_grid,
1614 clustering_percentile=clustering_percentile,
1615 screening_percentile=screening_percentile,
1616 scoring=scoring,
1617 smoothing_fwhm=smoothing_fwhm,
1618 standardize=standardize,
1619 target_affine=target_affine,
1620 target_shape=target_shape,
1621 low_pass=low_pass,
1622 high_pass=high_pass,
1623 t_r=t_r,
1624 mask_strategy=mask_strategy,
1625 memory=memory,
1626 is_classification=False,
1627 memory_level=memory_level,
1628 verbose=verbose,
1629 n_jobs=n_jobs,
1630 )
1632 # TODO remove after sklearn>=1.6
1633 self._estimator_type = "regressor"
1635 def _more_tags(self):
1636 """Return estimator tags.
1638 TODO remove when bumping sklearn_version > 1.5
1639 """
1640 return self.__sklearn_tags__()
1642 def __sklearn_tags__(self):
1643 """Return estimator tags.
1645 See the sklearn documentation for more details on tags
1646 https://scikit-learn.org/1.6/developers/develop.html#estimator-tags
1647 """
1648 # TODO
1649 # get rid of if block
1650 # bumping sklearn_version > 1.5
1651 # see https://github.com/scikit-learn/scikit-learn/pull/29677
1652 tags = super().__sklearn_tags__()
1653 if SKLEARN_LT_1_6:
1654 tags["multioutput"] = True
1655 return tags
1657 from sklearn.utils import RegressorTags
1659 tags.estimator_type = "regressor"
1660 tags.regressor_tags = RegressorTags()
1662 return tags
1664 @fill_doc
1665 def fit(self, X, y, groups=None):
1666 """Fit the decoder (learner).
1668 Parameters
1669 ----------
1670 X : list of Niimg-like or :obj:`~nilearn.surface.SurfaceImage` objects
1671 See :ref:`extracting_data`.
1672 Data on which model is to be fitted. If this is a list,
1673 the affine is considered the same for all.
1675 y : numpy.ndarray of shape=(n_samples) or list of length n_samples
1676 The dependent variable (age, sex, IQ, yes/no, etc.).
1677 Target variable to predict. Must have exactly as many elements as
1678 3D images in niimg.
1680 %(groups)s
1682 %(base_decoder_fit_attributes)s
1684 """
1685 check_params(self.__dict__)
1686 self.classes_ = ["beta"]
1687 super().fit(X, y, groups=groups)
1688 return self
1691@fill_doc
1692class FREMClassifier(_BaseDecoder):
1693 """State of the art :term:`decoding` scheme applied to usual classifiers.
1695 FREM uses an implicit spatial regularization through fast clustering and
1696 aggregates a high number of estimators trained on various splits of the
1697 training set, thus returning a very robust decoder
1698 at a lower computational cost
1699 than other spatially regularized methods :footcite:p:`Hoyos-Idrobo2018`.
1701 Parameters
1702 ----------
1703 estimator : :obj:`str`, default 'svc')
1704 The estimator to choose among:
1705 %(classifier_options)s
1708 mask : filename, Nifti1Image, NiftiMasker, or MultiNiftiMasker, optional,\
1709 default=None
1710 Mask to be used on data. If an instance of masker is passed,
1711 then its mask and parameters will be used. If no mask is given, mask
1712 will be computed automatically from provided images by an inbuilt
1713 masker with default parameters. Refer to NiftiMasker or
1714 MultiNiftiMasker to check for default parameters.
1716 cv : :obj:`int` or cross-validation generator, default=30
1717 If int, number of stratified shuffled splits returned, which is usually
1718 the right way to train many different classifiers. A good trade-off
1719 between stability of the aggregated model and computation time is
1720 50 splits. Shuffled splits are seeded by default for reproducibility.
1721 Can also be a cross-validation generator.
1723 param_grid : :obj:`dict` of :obj:`str` to sequence, or sequence of such. \
1724 default=None
1725 The parameter grid to explore, as a dictionary mapping estimator
1726 parameters to sequences of allowed values.
1728 None or an empty dict signifies default parameters.
1730 A sequence of dicts signifies a sequence of grids to search, and is
1731 useful to avoid exploring parameter combinations that make no sense
1732 or have no effect. See scikit-learn documentation for more information,
1733 for example: https://scikit-learn.org/stable/modules/grid_search.html
1735 clustering_percentile : :obj:`int`, :obj:`float`, \
1736 in closed interval [0, 100], \
1737 default=10
1738 Used to perform a fast ReNA clustering on input data as a first step of
1739 fit. It agglomerates similar features together to reduce their number
1740 down to this percentile. ReNA is typically efficient for
1741 cluster_percentile equal to 10.
1743 screening_percentile : :obj:`int`, :obj:`float`, \
1744 in closed interval [0, 100], \
1745 default=20
1746 The percentage of brain volume that will be kept with respect to a full
1747 MNI template. In particular, if it is lower than 100, a univariate
1748 feature selection based on the Anova F-value for the input data will be
1749 performed. A float according to a percentile of the highest
1750 scores.
1752 scoring : :obj:`str`, callable or None, optional. default='roc_auc'
1753 The scoring strategy to use. See the scikit-learn documentation at
1754 https://scikit-learn.org/stable/modules/model_evaluation.html#the-scoring-parameter-defining-model-evaluation-rules
1755 If callable, takes as arguments the fitted estimator, the
1756 test data (X_test) and the test target (y_test) if y is
1757 not None.
1758 e.g. scorer(estimator, X_test, y_test)
1760 For classification, valid entries are: 'accuracy', 'f1', 'precision',
1761 'recall' or 'roc_auc'; default='roc_auc'
1762 %(smoothing_fwhm)s
1763 %(standardize)s
1764 %(target_affine)s
1765 %(target_shape)s
1766 %(low_pass)s
1767 %(high_pass)s
1768 %(t_r)s
1769 %(mask_strategy)s
1771 .. note::
1772 This parameter will be ignored if a mask image is provided.
1774 .. note::
1775 Depending on this value, the mask will be computed from
1776 :func:`nilearn.masking.compute_background_mask`,
1777 :func:`nilearn.masking.compute_epi_mask`, or
1778 :func:`nilearn.masking.compute_brain_mask`.
1780 Default='background'.
1781 %(memory)s
1782 %(memory_level)s
1783 %(n_jobs)s
1784 %(verbose0)s
1786 References
1787 ----------
1788 .. footbibliography::
1790 See Also
1791 --------
1792 nilearn.decoding.Decoder: Classification strategies for Neuroimaging,
1793 nilearn.decoding.FREMRegressor: State of the art regression pipeline
1794 for Neuroimaging
1796 """
1798 def __init__(
1799 self,
1800 estimator="svc",
1801 mask=None,
1802 cv=30,
1803 param_grid=None,
1804 clustering_percentile=10,
1805 screening_percentile=20,
1806 scoring="roc_auc",
1807 smoothing_fwhm=None,
1808 standardize=True,
1809 target_affine=None,
1810 target_shape=None,
1811 mask_strategy="background",
1812 low_pass=None,
1813 high_pass=None,
1814 t_r=None,
1815 memory=None,
1816 memory_level=0,
1817 n_jobs=1,
1818 verbose=0,
1819 ):
1820 super().__init__(
1821 estimator=estimator,
1822 mask=mask,
1823 cv=cv,
1824 param_grid=param_grid,
1825 clustering_percentile=clustering_percentile,
1826 screening_percentile=screening_percentile,
1827 scoring=scoring,
1828 smoothing_fwhm=smoothing_fwhm,
1829 standardize=standardize,
1830 target_affine=target_affine,
1831 target_shape=target_shape,
1832 mask_strategy=mask_strategy,
1833 memory=memory,
1834 is_classification=True,
1835 memory_level=memory_level,
1836 verbose=verbose,
1837 n_jobs=n_jobs,
1838 low_pass=low_pass,
1839 high_pass=high_pass,
1840 t_r=t_r,
1841 )
1843 # TODO remove after sklearn>=1.6
1844 self._estimator_type = "classifier"
1846 def _more_tags(self):
1847 """Return estimator tags.
1849 TODO remove when bumping sklearn_version > 1.5
1850 """
1851 return self.__sklearn_tags__()
1853 def __sklearn_tags__(self):
1854 """Return estimator tags.
1856 See the sklearn documentation for more details on tags
1857 https://scikit-learn.org/1.6/developers/develop.html#estimator-tags
1858 """
1859 # TODO
1860 # get rid of if block
1861 # bumping sklearn_version > 1.5
1862 # see https://github.com/scikit-learn/scikit-learn/pull/29677
1863 tags = super().__sklearn_tags__()
1864 if SKLEARN_LT_1_6:
1865 return tags
1867 from sklearn.utils import ClassifierTags
1869 tags.estimator_type = "classifier"
1870 tags.classifier_tags = ClassifierTags()
1872 return tags