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

1from functools import partial 

2 

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 

11 

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 

29 

30from .test_same_api import to_niimgs 

31 

32logistic_path_scores = partial(path_scores, is_classif=True) 

33squared_loss_path_scores = partial(path_scores, is_classif=False) 

34 

35 

36IS_CLASSIF = [True, False] 

37 

38PENALTY = ["graph-net", "tv-l1"] 

39 

40 

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) 

49 

50 alpha_max = np.max(np.abs(np.dot(X.T, y))) / l1_ratio 

51 

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 ) 

59 

60 alphas = _space_net_alpha_grid( 

61 X, y, n_alphas=n_alphas, l1_ratio=l1_ratio, logistic=is_classif 

62 ) 

63 

64 assert_almost_equal(alphas.max(), alpha_max) 

65 assert_almost_equal(n_alphas, len(alphas)) 

66 

67 

68def test_space_net_alpha_grid_same_as_sk(): 

69 iris = load_iris() 

70 X = iris.data 

71 y = iris.target 

72 

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 ) 

77 

78 

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 

89 

90 # jitter 

91 if k > 0 and rng.random() > 0.9: 

92 w[k - 1] = 1 - w[k - 1] 

93 

94 escb({"w": w, "counter": counter}) 

95 assert len(escb.test_scores) == counter + 1 

96 

97 # restart 

98 if counter > 20: 

99 w *= 0.0 

100 

101 

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 ) 

122 

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 

128 

129 

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 ) 

144 

145 assert cvobj.alphas == alpha 

146 assert cvobj.l1_ratios == l1_ratio 

147 

148 

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) 

155 

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 

167 

168 

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] 

175 

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] 

188 

189 assert len(test_scores) == len(alphas) 

190 assert X.shape[1] + 1 == len(best_w) 

191 

192 

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] 

199 

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] 

211 

212 test_scores = test_scores[0] 

213 assert len(test_scores) == len(alphas) 

214 assert X.shape[1] + 1 == len(best_w) 

215 

216 

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) 

229 

230 alphas = [0.1, 1.0] 

231 

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) 

241 

242 

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) 

255 

256 with pytest.raises(ValueError, match="l1_ratio must be in the interval"): 

257 BaseSpaceNet(l1_ratios=l1_ratio).fit(X, y) 

258 

259 

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) 

274 

275 

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 

281 

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) 

289 

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) 

298 

299 

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)) 

305 

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) 

314 

315 accuracy = gnc.score(X_, y) 

316 assert accuracy == accuracy_score(y, gnc.predict(X_)) 

317 

318 

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. 

323 

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)) 

333 

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) 

344 

345 sklogreg = LogisticRegression( 

346 penalty="l1", fit_intercept=True, solver="liblinear", tol=tol, C=C 

347 ).fit(X, y) 

348 

349 # compare supports 

350 assert_array_equal( 

351 (np.abs(tvl1.coef_) < zero_thr), (np.abs(sklogreg.coef_) < zero_thr) 

352 ) 

353 

354 # compare predictions 

355 assert_array_equal(tvl1.predict(X_), sklogreg.predict(X)) 

356 

357 

358def test_lasso_vs_graph_net(): 

359 """Test for one of the extreme cases of Graph-Net. 

360 

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) 

369 

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) 

381 

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) 

387 

388 

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) 

394 

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) 

399 

400 

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) 

406 

407 assert mask.sum() >= 100.0 

408 

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) 

417 

418 X_, mask_, support_ = _univariate_feature_screening( 

419 X, y, mask, is_classif, 20.0 

420 ) 

421 n_features_ = support_.sum() 

422 

423 assert X_.shape[1] == n_features_ 

424 assert mask_.sum() == n_features_ 

425 assert n_features_ <= n_features 

426 

427 

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 ) 

440 

441 assert cvobj.alphas == alpha 

442 assert cvobj.l1_ratios == l1_ratio 

443 

444 

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 ) 

457 

458 assert cvobj.alphas == alpha 

459 assert cvobj.l1_ratios == l1_ratio 

460 

461 

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]) 

466 

467 assert not np.any( 

468 np.isnan( 

469 _space_net_alpha_grid(X, y, l1_ratio=0.0, logistic=is_classif) 

470 ) 

471 ) 

472 

473 

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) 

478 

479 

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]) 

486 

487 model(n_alphas=1, mask=mask).fit(X, y) 

488 model(n_alphas=2, mask=mask, alphas=None).fit(X, y) 

489 

490 

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)) 

497 

498 # Remove ten samples from y 

499 y = y[:-10] 

500 

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 ) 

512 

513 

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) 

519 

520 imgs, mask = to_niimgs(X, (2, 2, 2)) 

521 regressor = SpaceNetRegressor(mask=mask) 

522 

523 with pytest.raises( 

524 ValueError, match="The given input y must have at least 2 targets" 

525 ): 

526 regressor.fit(imgs, y) 

527 

528 

529# ------------------------ surface tests ------------------------------------ # 

530 

531 

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() 

542 

543 with pytest.raises(NotImplementedError): 

544 model(mask=surf_mask).fit(surf_img_2d(5), y) 

545 

546 with pytest.raises(NotImplementedError): 

547 model().fit(surf_img_2d(5), y)