Coverage for nilearn/decoding/tests/test_space_net.py: 0%
265 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
1from functools import partial
3import numpy as np
4import pytest
5from numpy.testing import assert_almost_equal, assert_array_equal
6from scipy import linalg
7from sklearn.datasets import load_iris
8from sklearn.linear_model import Lasso, LogisticRegression
9from sklearn.linear_model._coordinate_descent import _alpha_grid
10from sklearn.metrics import accuracy_score
12from nilearn._utils.param_validation import adjust_screening_percentile
13from nilearn.decoding.space_net import (
14 BaseSpaceNet,
15 SpaceNetClassifier,
16 SpaceNetRegressor,
17 _crop_mask,
18 _EarlyStoppingCallback,
19 _space_net_alpha_grid,
20 _univariate_feature_screening,
21 path_scores,
22)
23from nilearn.decoding.space_net_solvers import (
24 graph_net_logistic,
25 graph_net_squared_loss,
26)
27from nilearn.decoding.tests._testing import create_graph_net_simulation_data
28from nilearn.image import get_data
30from .test_same_api import to_niimgs
32logistic_path_scores = partial(path_scores, is_classif=True)
33squared_loss_path_scores = partial(path_scores, is_classif=False)
36IS_CLASSIF = [True, False]
38PENALTY = ["graph-net", "tv-l1"]
41@pytest.mark.parametrize("is_classif", IS_CLASSIF)
42@pytest.mark.parametrize("l1_ratio", [0.5, 0.99])
43@pytest.mark.parametrize("n_alphas", range(1, 10))
44def test_space_net_alpha_grid(
45 rng, is_classif, l1_ratio, n_alphas, n_samples=4, n_features=3
46):
47 X = rng.standard_normal((n_samples, n_features))
48 y = np.arange(n_samples)
50 alpha_max = np.max(np.abs(np.dot(X.T, y))) / l1_ratio
52 if n_alphas == 1:
53 assert_almost_equal(
54 _space_net_alpha_grid(
55 X, y, n_alphas=n_alphas, l1_ratio=l1_ratio, logistic=is_classif
56 ),
57 alpha_max,
58 )
60 alphas = _space_net_alpha_grid(
61 X, y, n_alphas=n_alphas, l1_ratio=l1_ratio, logistic=is_classif
62 )
64 assert_almost_equal(alphas.max(), alpha_max)
65 assert_almost_equal(n_alphas, len(alphas))
68def test_space_net_alpha_grid_same_as_sk():
69 iris = load_iris()
70 X = iris.data
71 y = iris.target
73 assert_almost_equal(
74 _space_net_alpha_grid(X, y, n_alphas=5),
75 X.shape[0] * _alpha_grid(X, y, n_alphas=5, fit_intercept=False),
76 )
79def test_early_stopping_callback_object(rng, n_samples=10, n_features=30):
80 # This test evolves w so that every line of th _EarlyStoppingCallback
81 # code is executed a some point. This a kind of code fuzzing.
82 X_test = rng.standard_normal((n_samples, n_features))
83 y_test = np.dot(X_test, np.ones(n_features))
84 w = np.zeros(n_features)
85 escb = _EarlyStoppingCallback(X_test, y_test, False)
86 for counter in range(50):
87 k = min(counter, n_features - 1)
88 w[k] = 1
90 # jitter
91 if k > 0 and rng.random() > 0.9:
92 w[k - 1] = 1 - w[k - 1]
94 escb({"w": w, "counter": counter})
95 assert len(escb.test_scores) == counter + 1
97 # restart
98 if counter > 20:
99 w *= 0.0
102@pytest.mark.parametrize("penalty", PENALTY)
103@pytest.mark.parametrize("is_classif", IS_CLASSIF)
104@pytest.mark.parametrize("n_alphas", [0.1, 0.01])
105@pytest.mark.parametrize("l1_ratio", [0.5, 0.99])
106@pytest.mark.parametrize("n_jobs", [1, -1])
107@pytest.mark.parametrize("cv", [2, 3])
108@pytest.mark.parametrize("perc", [5, 10])
109def test_params_correctly_propagated_in_constructors(
110 penalty, is_classif, n_alphas, l1_ratio, n_jobs, cv, perc
111):
112 cvobj = BaseSpaceNet(
113 mask="dummy",
114 n_alphas=n_alphas,
115 n_jobs=n_jobs,
116 l1_ratios=l1_ratio,
117 cv=cv,
118 screening_percentile=perc,
119 penalty=penalty,
120 is_classif=is_classif,
121 )
123 assert cvobj.n_alphas == n_alphas
124 assert cvobj.l1_ratios == l1_ratio
125 assert cvobj.n_jobs == n_jobs
126 assert cvobj.cv == cv
127 assert cvobj.screening_percentile == perc
130@pytest.mark.parametrize("penalty", PENALTY)
131@pytest.mark.parametrize("is_classif", IS_CLASSIF)
132@pytest.mark.parametrize("alpha", [0.4, 0.01])
133@pytest.mark.parametrize("l1_ratio", [0.5, 0.99])
134def test_params_correctly_propagated_in_constructors_biz(
135 penalty, is_classif, alpha, l1_ratio
136):
137 cvobj = BaseSpaceNet(
138 mask="dummy",
139 penalty=penalty,
140 is_classif=is_classif,
141 alphas=alpha,
142 l1_ratios=l1_ratio,
143 )
145 assert cvobj.alphas == alpha
146 assert cvobj.l1_ratios == l1_ratio
149def test_screening_space_net():
150 size = 4
151 X_, *_ = create_graph_net_simulation_data(
152 snr=1.0, n_samples=10, size=size, n_points=5, random_state=42
153 )
154 _, mask = to_niimgs(X_, [size] * 3)
156 for verbose in [0, 2]:
157 with pytest.warns(UserWarning):
158 screening_percentile = adjust_screening_percentile(
159 10, mask, verbose
160 )
161 with pytest.warns(UserWarning):
162 screening_percentile = adjust_screening_percentile(10, mask)
163 # We gave here a very small mask, judging by standards of brain size
164 # thus the screening_percentile_ corrected for brain size should
165 # be 100%
166 assert screening_percentile == 100
169def test_logistic_path_scores():
170 iris = load_iris()
171 X, y = iris.data, iris.target
172 _, mask = to_niimgs(X, [2, 2, 2])
173 mask = get_data(mask).astype(bool)
174 alphas = [1.0, 0.1, 0.01]
176 test_scores, best_w = logistic_path_scores(
177 graph_net_logistic,
178 X,
179 y,
180 mask,
181 alphas,
182 0.5,
183 np.arange(len(X)),
184 np.arange(len(X)),
185 {},
186 )[:2]
187 test_scores = test_scores[0]
189 assert len(test_scores) == len(alphas)
190 assert X.shape[1] + 1 == len(best_w)
193def test_squared_loss_path_scores():
194 iris = load_iris()
195 X, y = iris.data, iris.target
196 _, mask = to_niimgs(X, [2, 2, 2])
197 mask = get_data(mask).astype(bool)
198 alphas = [1.0, 0.1, 0.01]
200 test_scores, best_w = squared_loss_path_scores(
201 graph_net_squared_loss,
202 X,
203 y,
204 mask,
205 alphas,
206 0.5,
207 np.arange(len(X)),
208 np.arange(len(X)),
209 {},
210 )[:2]
212 test_scores = test_scores[0]
213 assert len(test_scores) == len(alphas)
214 assert X.shape[1] + 1 == len(best_w)
217@pytest.mark.parametrize("l1_ratio", [0.99])
218@pytest.mark.parametrize("debias", [True])
219def test_tv_regression_simple(rng, l1_ratio, debias):
220 dim = (4, 4, 4)
221 W_init = np.zeros(dim)
222 W_init[2:3, 1:2, -2:] = 1
223 n = 10
224 p = np.prod(dim)
225 X = np.ones((n, 1)) + W_init.ravel().T
226 X += rng.standard_normal((n, p))
227 y = np.dot(X, W_init.ravel())
228 X, mask = to_niimgs(X, dim)
230 alphas = [0.1, 1.0]
232 BaseSpaceNet(
233 mask=mask,
234 alphas=alphas,
235 l1_ratios=l1_ratio,
236 penalty="tv-l1",
237 is_classif=False,
238 max_iter=10,
239 debias=debias,
240 ).fit(X, y)
243@pytest.mark.parametrize("l1_ratio", [-2, 2])
244def test_base_estimator_invalid_l1_ratio(rng, l1_ratio):
245 """Check that 0 < L1 ratio < 1."""
246 dim = (4, 4, 4)
247 W_init = np.zeros(dim)
248 W_init[2:3, 1:2, -2:] = 1
249 n = 10
250 p = np.prod(dim)
251 X = np.ones((n, 1)) + W_init.ravel().T
252 X += rng.standard_normal((n, p))
253 y = np.dot(X, W_init.ravel())
254 X, _ = to_niimgs(X, dim)
256 with pytest.raises(ValueError, match="l1_ratio must be in the interval"):
257 BaseSpaceNet(l1_ratios=l1_ratio).fit(X, y)
260@pytest.mark.parametrize("penalty_wrong_case", ["Graph-Net", "TV-L1"])
261def test_string_params_case(rng, penalty_wrong_case):
262 """Check value of penalty."""
263 dim = (4, 4, 4)
264 W_init = np.zeros(dim)
265 W_init[2:3, 1:2, -2:] = 1
266 n = 10
267 p = np.prod(dim)
268 X = np.ones((n, 1)) + W_init.ravel().T
269 X += rng.standard_normal((n, p))
270 y = np.dot(X, W_init.ravel())
271 X, _ = to_niimgs(X, dim)
272 with pytest.raises(ValueError, match="'penalty' parameter .* be one of"):
273 BaseSpaceNet(penalty=penalty_wrong_case).fit(X, y)
276@pytest.mark.parametrize("l1_ratio", [0.01, 0.5, 0.99])
277def test_tv_regression_3d_image_doesnt_crash(rng, l1_ratio):
278 dim = (3, 4, 5)
279 W_init = np.zeros(dim)
280 W_init[2:3, 3:, 1:3] = 1
282 n = 10
283 p = dim[0] * dim[1] * dim[2]
284 X = np.ones((n, 1)) + W_init.ravel().T
285 X += rng.standard_normal((n, p))
286 y = np.dot(X, W_init.ravel())
287 alpha = 1.0
288 X, mask = to_niimgs(X, dim)
290 BaseSpaceNet(
291 mask=mask,
292 alphas=alpha,
293 l1_ratios=l1_ratio,
294 penalty="tv-l1",
295 is_classif=False,
296 max_iter=10,
297 ).fit(X, y)
300def test_graph_net_classifier_score():
301 iris = load_iris()
302 X, y = iris.data, iris.target
303 y = 2 * (y > 0) - 1
304 X_, mask = to_niimgs(X, (2, 2, 2))
306 gnc = SpaceNetClassifier(
307 mask=mask,
308 alphas=1.0 / 0.01 / X.shape[0],
309 l1_ratios=1.0,
310 tol=1e-10,
311 standardize=False,
312 screening_percentile=100.0,
313 ).fit(X_, y)
315 accuracy = gnc.score(X_, y)
316 assert accuracy == accuracy_score(y, gnc.predict(X_))
319def test_log_reg_vs_graph_net_two_classes_iris(
320 C=0.01, tol=1e-10, zero_thr=1e-4
321):
322 """Test for one of the extreme cases of Graph-Net.
324 That is, with l1_ratio = 1 (pure Lasso),
325 we compare Graph-Net's coefficients'
326 performance with the coefficients obtained from Scikit-Learn's
327 LogisticRegression, with L1 penalty, in a 2 classes classification task.
328 """
329 iris = load_iris()
330 X, y = iris.data, iris.target
331 y = 2 * (y > 0) - 1
332 X_, mask = to_niimgs(X, (2, 2, 2))
334 tvl1 = SpaceNetClassifier(
335 mask=mask,
336 alphas=1.0 / C / X.shape[0],
337 l1_ratios=1.0,
338 tol=tol,
339 max_iter=1000,
340 penalty="tv-l1",
341 standardize=False,
342 screening_percentile=100.0,
343 ).fit(X_, y)
345 sklogreg = LogisticRegression(
346 penalty="l1", fit_intercept=True, solver="liblinear", tol=tol, C=C
347 ).fit(X, y)
349 # compare supports
350 assert_array_equal(
351 (np.abs(tvl1.coef_) < zero_thr), (np.abs(sklogreg.coef_) < zero_thr)
352 )
354 # compare predictions
355 assert_array_equal(tvl1.predict(X_), sklogreg.predict(X))
358def test_lasso_vs_graph_net():
359 """Test for one of the extreme cases of Graph-Net.
361 That is, with l1_ratio = 1 (pure Lasso),
362 we compare Graph-Net's performance with Scikit-Learn lasso
363 """
364 size = 4
365 X_, y, _, mask = create_graph_net_simulation_data(
366 snr=1.0, n_samples=10, size=size, n_points=5, random_state=10
367 )
368 X, mask = to_niimgs(X_, [size] * 3)
370 lasso = Lasso(max_iter=100, tol=1e-8)
371 graph_net = BaseSpaceNet(
372 mask=mask,
373 alphas=1.0 * X_.shape[0],
374 l1_ratios=1,
375 is_classif=False,
376 penalty="graph-net",
377 max_iter=100,
378 )
379 lasso.fit(X_, y)
380 graph_net.fit(X, y)
382 lasso_perf = 0.5 / y.size * linalg.norm(
383 np.dot(X_, lasso.coef_) - y
384 ) ** 2 + np.sum(np.abs(lasso.coef_))
385 graph_net_perf = 0.5 * ((graph_net.predict(X) - y) ** 2).mean()
386 assert_almost_equal(graph_net_perf, lasso_perf, decimal=2)
389def test_crop_mask(rng):
390 mask = np.zeros((3, 4, 5), dtype=bool)
391 box = mask[:2, :3, :4]
392 box[rng.random(box.shape) < 3.0] = 1 # mask covers 30% of brain
393 idx = np.where(mask)
395 assert idx[1].max() < 3
396 tight_mask = _crop_mask(mask)
397 assert mask.sum() == tight_mask.sum()
398 assert np.prod(tight_mask.shape) <= np.prod(box.shape)
401@pytest.mark.parametrize("is_classif", IS_CLASSIF)
402def test_univariate_feature_screening(
403 rng, is_classif, dim=(11, 12, 13), n_samples=10
404):
405 mask = rng.random(dim) > 100.0 / np.prod(dim)
407 assert mask.sum() >= 100.0
409 mask[dim[0] // 2, dim[1] // 3 :, -dim[2] // 2 :] = (
410 1 # put spatial structure
411 )
412 n_features = mask.sum()
413 X = rng.standard_normal((n_samples, n_features))
414 w = rng.standard_normal(n_features)
415 w[rng.random(n_features) > 0.8] = 0.0
416 y = X.dot(w)
418 X_, mask_, support_ = _univariate_feature_screening(
419 X, y, mask, is_classif, 20.0
420 )
421 n_features_ = support_.sum()
423 assert X_.shape[1] == n_features_
424 assert mask_.sum() == n_features_
425 assert n_features_ <= n_features
428@pytest.mark.parametrize("penalty", PENALTY)
429@pytest.mark.parametrize("alpha", [0.4, 0.01])
430@pytest.mark.parametrize("l1_ratio", [0.5, 0.99])
431@pytest.mark.parametrize("verbose", [True, False])
432def test_space_net_classifier_subclass(penalty, alpha, l1_ratio, verbose):
433 cvobj = SpaceNetClassifier(
434 mask="dummy",
435 penalty=penalty,
436 alphas=alpha,
437 l1_ratios=l1_ratio,
438 verbose=verbose,
439 )
441 assert cvobj.alphas == alpha
442 assert cvobj.l1_ratios == l1_ratio
445@pytest.mark.parametrize("penalty", PENALTY)
446@pytest.mark.parametrize("alpha", [0.4, 0.01])
447@pytest.mark.parametrize("l1_ratio", [0.5, 0.99])
448@pytest.mark.parametrize("verbose", [True, False])
449def test_space_net_regressor_subclass(penalty, alpha, l1_ratio, verbose):
450 cvobj = SpaceNetRegressor(
451 mask="dummy",
452 penalty=penalty,
453 alphas=alpha,
454 l1_ratios=l1_ratio,
455 verbose=verbose,
456 )
458 assert cvobj.alphas == alpha
459 assert cvobj.l1_ratios == l1_ratio
462@pytest.mark.parametrize("is_classif", IS_CLASSIF)
463def test_space_net_alpha_grid_pure_spatial(rng, is_classif):
464 X = rng.standard_normal((10, 100))
465 y = np.arange(X.shape[0])
467 assert not np.any(
468 np.isnan(
469 _space_net_alpha_grid(X, y, l1_ratio=0.0, logistic=is_classif)
470 )
471 )
474@pytest.mark.parametrize("mask_empty", [np.array([]), np.zeros((2, 2, 2))])
475def test_crop_mask_empty_mask(mask_empty):
476 with pytest.raises(ValueError, match="Empty mask:."):
477 _crop_mask(mask_empty)
480@pytest.mark.parametrize("model", [SpaceNetRegressor, SpaceNetClassifier])
481def test_space_net_one_alpha_no_crash(model):
482 """Regression test."""
483 iris = load_iris()
484 X, y = iris.data, iris.target
485 X, mask = to_niimgs(X, [2, 2, 2])
487 model(n_alphas=1, mask=mask).fit(X, y)
488 model(n_alphas=2, mask=mask, alphas=None).fit(X, y)
491@pytest.mark.parametrize("model", [SpaceNetRegressor, SpaceNetClassifier])
492def test_checking_inputs_length(model):
493 iris = load_iris()
494 X, y = iris.data, iris.target
495 y = 2 * (y > 0) - 1
496 X_, mask = to_niimgs(X, (2, 2, 2))
498 # Remove ten samples from y
499 y = y[:-10]
501 with pytest.raises(ValueError, match="inconsistent numbers of samples"):
502 model(
503 mask=mask,
504 alphas=1.0 / 0.01 / X.shape[0],
505 l1_ratios=1.0,
506 tol=1e-10,
507 screening_percentile=100.0,
508 ).fit(
509 X_,
510 y,
511 )
514def test_targets_in_y_space_net_regressor():
515 """Raise an error when unique targets given in y are single."""
516 iris = load_iris()
517 X, _ = iris.data, iris.target
518 y = np.ones(iris.target.shape)
520 imgs, mask = to_niimgs(X, (2, 2, 2))
521 regressor = SpaceNetRegressor(mask=mask)
523 with pytest.raises(
524 ValueError, match="The given input y must have at least 2 targets"
525 ):
526 regressor.fit(imgs, y)
529# ------------------------ surface tests ------------------------------------ #
532@pytest.mark.parametrize("surf_mask_dim", [1, 2])
533@pytest.mark.parametrize(
534 "model", [BaseSpaceNet, SpaceNetRegressor, SpaceNetClassifier]
535)
536def test_space_net_not_implemented_surface_objects(
537 surf_mask_dim, surf_mask_1d, surf_mask_2d, surf_img_2d, model
538):
539 """Raise NotImplementedError when space net is fit on surface objects."""
540 y = np.ones((5,))
541 surf_mask = surf_mask_1d if surf_mask_dim == 1 else surf_mask_2d()
543 with pytest.raises(NotImplementedError):
544 model(mask=surf_mask).fit(surf_img_2d(5), y)
546 with pytest.raises(NotImplementedError):
547 model().fit(surf_img_2d(5), y)