GroupMultiNeSS 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,524 @@
1
+ import numpy as np
2
+ from typing import List, Dict, Union, Tuple
3
+ from collections import Counter
4
+ from functools import partial
5
+ from joblib import Parallel, delayed
6
+ from warnings import warn
7
+
8
+ from .multiness import BaseMultiNeSS, BaseMultiNeSSRefined, BaseMultiNeSSRefinedCV, BaseOracleMultiNeSS
9
+ from .base import BaseRefitting
10
+ from .utils import if_scalar_or_given_length_array, fill_diagonals, \
11
+ soft_thresholding_operator
12
+
13
+
14
+
15
+ class BaseGroupMultiNeSS(BaseMultiNeSS):
16
+ """
17
+ Base class for group-structured MultiNeSS models.
18
+
19
+ Handles shared, group-specific, and individual latent spaces for network layers.
20
+
21
+ Parameters
22
+ ----------
23
+ group_indices : List[int]
24
+ List of group assignments for each network layer.
25
+ edge_distrib : str, default='normal'
26
+ Distribution of edges ('normal' or 'bernoulli').
27
+ loops_allowed : bool, default=True
28
+ Whether self-loops are allowed in the network.
29
+ """
30
+
31
+ def __init__(self, group_indices: List[int],
32
+ edge_distrib: str = "normal",
33
+ loops_allowed: bool = True):
34
+ """
35
+ Initialize BaseGroupMultiNeSS with group assignments.
36
+ """
37
+ super().__init__(edge_distrib=edge_distrib, loops_allowed=loops_allowed)
38
+ self.group_indices = group_indices
39
+
40
+ def compute_latent_spaces(self):
41
+ """
42
+ Compute the full latent space by summing shared, group-specific, and individual components.
43
+
44
+ Returns
45
+ -------
46
+ np.ndarray
47
+ The combined latent space matrix.
48
+ """
49
+ ls = self.get_shared_latent_space() + self.get_group_latent_spaces()[self.group_indices] + \
50
+ self.get_individual_latent_spaces()
51
+ if not self.loops_allowed:
52
+ fill_diagonals(ls, val=0., inplace=True)
53
+ return ls
54
+
55
+ def get_shared_latent_space(self):
56
+ """
57
+ Get the shared latent space.
58
+
59
+ Returns
60
+ -------
61
+ np.ndarray
62
+ Shared component matrix.
63
+ """
64
+ return self.get_all_fitted_matrices()[0]
65
+
66
+ def get_group_latent_spaces(self):
67
+ """
68
+ Get the group-specific latent spaces.
69
+
70
+ Returns
71
+ -------
72
+ np.ndarray
73
+ Group component matrices.
74
+ """
75
+ return self.get_all_fitted_matrices()[1: self.n_groups_ + 1]
76
+
77
+ def get_individual_latent_spaces(self):
78
+ """
79
+ Get the individual latent spaces.
80
+
81
+ Returns
82
+ -------
83
+ np.ndarray
84
+ Individual component matrices.
85
+ """
86
+ return self.get_all_fitted_matrices()[self.n_groups_ + 1:]
87
+
88
+ def get_all_fitted_matrices_by_type(self):
89
+ """
90
+ Return all fitted matrices separated by type: shared, group, individual.
91
+
92
+ Returns
93
+ -------
94
+ list
95
+ List of matrices by type.
96
+ """
97
+ return [self.get_shared_latent_space(), self.get_group_latent_spaces(), self.get_individual_latent_spaces()]
98
+
99
+ @staticmethod
100
+ def _get_all_fitted_parameter_names():
101
+ """
102
+ Names of the fitted parameter groups.
103
+
104
+ Returns
105
+ -------
106
+ list
107
+ List of strings representing component types.
108
+ """
109
+ return ["Shared component", "Group components", "Individual components"]
110
+
111
+ def _validate_input(self, As: List[np.array]):
112
+ """
113
+ Validate input data and group assignments.
114
+
115
+ Parameters
116
+ ----------
117
+ As : List[np.ndarray]
118
+ List of network adjacency matrices.
119
+
120
+ Returns
121
+ -------
122
+ np.ndarray
123
+ Validated adjacency matrices.
124
+ """
125
+ if np.any([group_size == 1 for group_size in Counter(self.group_indices).values()]):
126
+ warn("Group latent space is unidentifiable for groups of size 1.")
127
+ self.unique_groups_ = np.sort(np.unique(self.group_indices))
128
+ self.group_sizes_ = np.array([(self.group_indices == group).sum() for group in self.unique_groups_])
129
+ self.n_groups_ = len(self.unique_groups_)
130
+ assert np.all(np.sort(self.unique_groups_) == np.arange(len(self.unique_groups_))), \
131
+ "Groups should be named from 0 to K-1, where K is the number of unique groups."
132
+ As = super()._validate_input(As)
133
+ assert len(self.group_indices) == len(As), "Length of group_indices should be equal to the number of layers"
134
+ return As
135
+
136
+
137
+ class BaseGroupMultiNeSSRefined(BaseGroupMultiNeSS, BaseMultiNeSSRefined):
138
+ """
139
+ Refined group-structured MultiNeSS model.
140
+
141
+ Inherits from BaseGroupMultiNeSS and BaseMultiNeSSRefined.
142
+ Handles optimization over shared, group-specific, and individual components.
143
+
144
+ Parameters
145
+ ----------
146
+ group_indices : List[int]
147
+ List of group assignments for each network layer.
148
+ edge_distrib : str, default='normal'
149
+ Edge distribution.
150
+ max_rank : int, optional
151
+ Maximum rank for low-rank components.
152
+ init_rank : int, optional
153
+ Initial rank for low-rank components.
154
+ sigmas : float, list, or np.ndarray, optional
155
+ Noise standard deviation(s).
156
+ loops_allowed : bool, default=True
157
+ Whether self-loops are allowed.
158
+ n_jobs : int, optional
159
+ Number of parallel jobs for optimization.
160
+ """
161
+
162
+ def __init__(self, group_indices: List[int],
163
+ edge_distrib: str = "normal",
164
+ max_rank: int = None, init_rank: int = None,
165
+ sigmas: Union[float, List, np.ndarray] = None,
166
+ loops_allowed: bool = True,
167
+ n_jobs: int = None):
168
+ """
169
+ Initialize BaseGroupMultiNeSSRefined.
170
+ """
171
+ BaseMultiNeSSRefined.__init__(self, edge_distrib=edge_distrib, loops_allowed=loops_allowed,
172
+ max_rank=max_rank, init_rank=init_rank, sigmas=sigmas)
173
+ self.group_indices = group_indices
174
+ self.n_jobs = n_jobs
175
+
176
+ @property
177
+ def n_fitted_matrices(self) -> int:
178
+ """
179
+ Total number of fitted matrices (shared + group + individual).
180
+
181
+ Returns
182
+ -------
183
+ int
184
+ Number of fitted matrices.
185
+ """
186
+ return 1 + self.n_layers_ + self.n_groups_
187
+
188
+ def _get_group_dependent_matrix_indices(self, group) -> List:
189
+ """
190
+ Get indices of matrices dependent on a specific group.
191
+
192
+ Parameters
193
+ ----------
194
+ group : int
195
+ Group index.
196
+
197
+ Returns
198
+ -------
199
+ list
200
+ List of matrix indices for the group.
201
+ """
202
+ individs_within_group_indices = 1 + self.n_groups_ + np.arange(self.n_layers_)[self.group_indices == group]
203
+ return [1 + group, *individs_within_group_indices]
204
+
205
+ def _get_param_participance_masks(self):
206
+ """
207
+ Get boolean masks indicating parameter participation for each component.
208
+
209
+ Returns
210
+ -------
211
+ np.ndarray
212
+ Masks array of shape (n_params, n_layers).
213
+ """
214
+ group_masks = [self.group_indices == group for group in self.unique_groups_]
215
+ return np.stack([np.ones(self.n_layers_, dtype=bool),
216
+ *group_masks,
217
+ *np.eye(self.n_layers_, dtype=bool)])
218
+
219
+ def _make_key_2_history_key_dict(self):
220
+ """
221
+ Map optimization keys to human-readable history keys.
222
+ """
223
+ self._key_2_history_key = {**{(0, group): f"Stage 1, Group {group}" for group in self.unique_groups_},
224
+ **{(1, 0): "Stage 2"}}
225
+
226
+ def _make_key_2_param_indices_dict(self):
227
+ """
228
+ Map optimization keys to parameter indices.
229
+ """
230
+ self._key_2_param_indices = {**{(0, group): self._get_group_dependent_matrix_indices(group)
231
+ for group in self.unique_groups_},
232
+ **{(1, 0): list(range(1 + self.n_groups_))}}
233
+
234
+ def _update_latent_history(self, key: Tuple[int, int]) -> None:
235
+ """
236
+ Update stored latent history for a given key.
237
+
238
+ Parameters
239
+ ----------
240
+ key : tuple
241
+ Optimization stage and group key.
242
+ """
243
+ stage, group = key
244
+ history_key = self._key_2_history_key[key]
245
+ if stage == 0:
246
+ self.latent_history_[history_key].append(
247
+ (self.get_group_latent_spaces()[group].copy(),
248
+ self.get_individual_latent_spaces()[self.group_indices == group].copy()))
249
+ else:
250
+ self.latent_history_[history_key].append((self.get_shared_latent_space(), self.get_group_latent_spaces()))
251
+
252
+ def _fit(self, As, lr: Union[float, Tuple[float, float]] = 1., **optim_kwargs):
253
+ """
254
+ Fit the model using two-stage optimization for groups and shared components.
255
+
256
+ Parameters
257
+ ----------
258
+ As : np.ndarray
259
+ Input adjacency matrices.
260
+ lr : float or tuple of float, default=1.
261
+ Learning rates for stage 1 and 2.
262
+ """
263
+ self._pre_fit()
264
+ lr = if_scalar_or_given_length_array(lr, length=2, name="lr")
265
+ Parallel(n_jobs=self.n_jobs)(
266
+ [delayed(self._optimize_params)(key=(0, group),
267
+ As=As, lr=lr[0],
268
+ **optim_kwargs)
269
+ for group in self.unique_groups_])
270
+ self._optimize_params(key=(1, 0), As=As, lr=lr[1], **optim_kwargs)
271
+
272
+
273
+ class GroupMultiNeSS(BaseGroupMultiNeSSRefined, BaseRefitting):
274
+ """
275
+ Group-structured MultiNeSS model with refitting capability.
276
+
277
+ Parameters
278
+ ----------
279
+ group_indices : List[int]
280
+ List of group assignments for each network layer.
281
+ lmbda1, lmbda2 : float or array-like
282
+ Regularization parameters for stage 1 (group) and stage 2 (shared) components.
283
+ alpha1, alpha2 : float or array-like
284
+ Weighting parameters for stage 1 (group) and stage 2 (shared) components.
285
+ edge_distrib : str, default='normal'
286
+ Edge distribution.
287
+ max_rank, init_rank : int, optional
288
+ Maximum and initial ranks for low-rank components.
289
+ sigmas : float, list, or np.ndarray, optional
290
+ Noise standard deviation(s).
291
+ loops_allowed : bool, default=True
292
+ Whether self-loops are allowed.
293
+ n_jobs : int, optional
294
+ Number of parallel jobs for optimization.
295
+ refit_threshold : float, default=1e-8
296
+ Threshold for refitting components.
297
+ """
298
+
299
+ def __init__(self, group_indices: List[int],
300
+ lmbda1: Union[float, np.ndarray, list] = None,
301
+ lmbda2: float = None,
302
+ alpha1: Union[float, np.ndarray, list] = None,
303
+ alpha2: Union[float, np.ndarray, list] = None,
304
+ edge_distrib: str = "normal",
305
+ max_rank: int = None, init_rank: int = None,
306
+ sigmas: Union[float, List, np.ndarray] = None,
307
+ loops_allowed: bool = True,
308
+ n_jobs: int = None,
309
+ refit_threshold: float = 1e-8):
310
+ """
311
+ Initialize GroupMultiNeSS model.
312
+ """
313
+ BaseGroupMultiNeSSRefined.__init__(self, group_indices=group_indices, edge_distrib=edge_distrib,
314
+ max_rank=max_rank, init_rank=init_rank, sigmas=sigmas,
315
+ loops_allowed=loops_allowed, n_jobs=n_jobs)
316
+ BaseRefitting.__init__(self, edge_distrib=edge_distrib, max_rank=max_rank, loops_allowed=loops_allowed,
317
+ refit_threshold=refit_threshold)
318
+ self.lmbda1 = lmbda1
319
+ self.lmbda2 = lmbda2
320
+ self.alpha1 = alpha1
321
+ self.alpha2 = alpha2
322
+
323
+ def _compute_projected_map(self, idx: int, lr, key: Tuple[int, int]):
324
+ """
325
+ Compute projected map for soft-thresholding operator.
326
+
327
+ Parameters
328
+ ----------
329
+ idx : int
330
+ Index of the parameter.
331
+ lr : float
332
+ Learning rate.
333
+ key : tuple
334
+ Optimization stage and group key.
335
+
336
+ Returns
337
+ -------
338
+ partial
339
+ Soft-thresholding operator with scaled threshold.
340
+ """
341
+ mask = self._param_participance_masks[idx]
342
+ penalty = self._compute_penalty_coef(idx=idx, key=key)
343
+ return partial(soft_thresholding_operator, threshold=penalty * lr / mask.sum(), max_rank=self.max_rank)
344
+
345
+ def _compute_penalty_coef(self, idx: int, key: Tuple[int, int]) -> float:
346
+ """
347
+ Compute penalty coefficient for a given parameter and stage.
348
+
349
+ Parameters
350
+ ----------
351
+ idx : int
352
+ Parameter index.
353
+ key : tuple
354
+ Stage and group key.
355
+
356
+ Returns
357
+ -------
358
+ float
359
+ Penalty coefficient.
360
+ """
361
+ hyperparam_dict = self._key_2_hyperparams_dict[key]
362
+ stage, group = key
363
+ lmbda, alpha = hyperparam_dict[f"lmbda{stage + 1}"], hyperparam_dict[f"alpha{stage + 1}"]
364
+ if stage == 0:
365
+ if idx == group + 1:
366
+ return lmbda
367
+ else:
368
+ idx_in_group = np.sum(self.group_indices[:idx - self.n_groups_ - 1] == group)
369
+ return lmbda * alpha[idx_in_group]
370
+ else:
371
+ return lmbda if idx == 0 else lmbda * alpha[idx - 1]
372
+
373
+ def _make_key_2_hyperparams_dict(self):
374
+ """
375
+ Create dictionary mapping optimization keys to hyperparameters.
376
+ """
377
+ if self.lmbda1 is None:
378
+ lmbda1 = 2. * np.sqrt(self.group_sizes_ * self.n_nodes_)
379
+ else:
380
+ lmbda1 = if_scalar_or_given_length_array(self.lmbda1, length=self.n_groups_, name="lmbda1")
381
+ lmbda2 = 2. * self.n_layers_ * np.sqrt(self.n_nodes_ / self.n_groups_) if self.lmbda2 is None else self.lmbda2
382
+ if self.alpha1 is None:
383
+ alpha1 = np.ones(self.n_layers_) / np.sqrt(self.group_sizes_[self.group_indices])
384
+ else:
385
+ alpha1 = if_scalar_or_given_length_array(self.alpha1, length=self.n_layers_, name="alpha1")
386
+ if self.alpha2 is None:
387
+ alpha2 = self.group_sizes_ / self.n_layers_ * np.sqrt(self.n_groups_)
388
+ else:
389
+ alpha2 = if_scalar_or_given_length_array(self.alpha2, length=self.n_groups_, name="alpha2")
390
+
391
+ self._key_2_hyperparams_dict = \
392
+ {**{(0, group): {"lmbda1": lmbda1[group], "alpha1": alpha1[self.group_indices == group]}
393
+ for group in self.unique_groups_},
394
+ **{(1, 0): {"lmbda2": lmbda2, "alpha2": alpha2}}}
395
+
396
+ def fit(self, As: List[np.array],
397
+ lr: Union[float, Tuple[float, float]] = 1e-1,
398
+ max_iter: int = 100, tol=1e-5, patience=10,
399
+ refit=True, verbose: bool = False, verbose_interval=1,
400
+ save_latent_history=False, save_nll_history=True):
401
+ """
402
+ Fit the GroupMultiNeSS model.
403
+
404
+ Parameters
405
+ ----------
406
+ As : np.ndarray
407
+ Input adjacency matrices.
408
+ lr : float or tuple, default=0.1
409
+ Learning rate(s) for stages.
410
+ max_iter : int, default=100
411
+ Maximum number of iterations.
412
+ tol : float, default=1e-5
413
+ Convergence tolerance.
414
+ patience : int, default=10
415
+ Early stopping patience.
416
+ refit : bool, default=True
417
+ Whether to refit components.
418
+ verbose : bool, default=False
419
+ Verbosity flag.
420
+ verbose_interval : int, default=1
421
+ Interval for printing verbose messages.
422
+ save_latent_history : bool, default=False
423
+ Whether to save latent history.
424
+ save_nll_history : bool, default=True
425
+ Whether to save NLL history.
426
+
427
+ Returns
428
+ -------
429
+ self
430
+ Fitted model instance.
431
+ """
432
+ As = self._validate_input(As=As)
433
+ self._fit(As, lr=lr,
434
+ max_iter=max_iter, tol=tol, patience=patience,
435
+ refit=refit,
436
+ verbose=verbose, verbose_interval=verbose_interval,
437
+ save_latent_history=save_latent_history, save_nll_history=save_nll_history)
438
+ return self
439
+
440
+
441
+ class GroupMultiNeSSCV(GroupMultiNeSS, BaseMultiNeSSRefinedCV):
442
+ def __init__(self, group_indices: List[int],
443
+ param_grid: Dict[str, List] = None,
444
+ edge_distrib: str = "normal",
445
+ lmbda1: Union[float, np.ndarray, list] = None,
446
+ lmbda2: float = None,
447
+ alpha1: Union[float, np.ndarray, list] = None,
448
+ alpha2: Union[float, np.ndarray, list] = None,
449
+ loops_allowed: bool = True,
450
+ cv_folds=3, test_prop=0.1,
451
+ max_rank: int = None, init_rank: int = None,
452
+ sigmas: Union[float, List, np.ndarray] = None,
453
+ n_jobs: int = None,
454
+ refit_threshold=1e-8,
455
+ fix_layer_split: bool = False,
456
+ use_1se_rule: bool = False,
457
+ random_seed: int = None):
458
+
459
+ BaseMultiNeSSRefinedCV.__init__(self, param_grid=param_grid, cv_folds=cv_folds, test_prop=test_prop,
460
+ loops_allowed=loops_allowed,
461
+ fix_layer_split=fix_layer_split,
462
+ use_1se_rule=use_1se_rule,
463
+ random_seed=random_seed)
464
+
465
+ GroupMultiNeSS.__init__(self, group_indices=group_indices,
466
+ lmbda1=lmbda1, lmbda2=lmbda2, alpha1=alpha1, alpha2=alpha2, edge_distrib=edge_distrib,
467
+ max_rank=max_rank, init_rank=init_rank, sigmas=sigmas,
468
+ loops_allowed=loops_allowed, n_jobs=n_jobs,
469
+ refit_threshold=refit_threshold)
470
+
471
+ def _make_key_2_hyperparams_grid_dict(self) -> None:
472
+ const_range = np.array([0.03, 0.1, 0.3, 1, 3, 10])
473
+ if self.param_grid is None:
474
+ self._key_2_hyperparams_grid = {
475
+ **{(1, 0): {"lmbda2": self.n_layers_ * np.sqrt(self.n_nodes_ / self.n_groups_) * const_range}},
476
+ **{(0, group): {"lmbda1": np.sqrt(self.n_nodes_ * self.group_sizes_[group]) * const_range}
477
+ for group in self.unique_groups_}}
478
+ else:
479
+
480
+ stage_2_grid = {stage: {key: grid for key, grid in self.param_grid.items() if key.endswith(str(stage + 1))}
481
+ for stage in range(2)}
482
+ self._key_2_hyperparams_grid = {**{(0, group): stage_2_grid[0] for group in self.unique_groups_},
483
+ **{(1, 0): stage_2_grid[1]}}
484
+
485
+
486
+ class OracleGroupMultiNeSS(BaseGroupMultiNeSSRefined, BaseOracleMultiNeSS):
487
+ def __init__(self, group_indices: List[int],
488
+ d_shared: int, d_groups: List[int], d_individs: List[int],
489
+ edge_distrib: str = "normal",
490
+ sigmas: Union[float, List, np.ndarray] = None,
491
+ loops_allowed: bool = True,
492
+ n_jobs: int = None):
493
+
494
+ BaseGroupMultiNeSSRefined.__init__(self, group_indices=group_indices,
495
+ edge_distrib=edge_distrib, sigmas=sigmas,
496
+ loops_allowed=loops_allowed, n_jobs=n_jobs)
497
+ self.d_shared = d_shared
498
+ self.d_groups = d_groups
499
+ self.d_individs = d_individs
500
+
501
+ def _make_key_2_hyperparams_dict(self):
502
+ d_shared = if_scalar_or_given_length_array(self.d_shared, length=1, name="d_shared")[0]
503
+ d_groups = if_scalar_or_given_length_array(self.d_groups, length=self.n_groups_, name="d_groups")
504
+ d_individs = if_scalar_or_given_length_array(self.d_individs, length=self.n_layers_, name="d_individs")
505
+ self._key_2_hyperparams_dict = {**{(0, group): {"d_shared": d_shared + d_groups[group],
506
+ "d_individs": d_individs[self.group_indices == group]}
507
+ for group in self.unique_groups_},
508
+ **{(1, 0): {"d_shared": d_shared, "d_individs": d_groups}}}
509
+
510
+ def _compute_threshold_rank(self, idx: int, key: Tuple[int, int]) -> int:
511
+ hyperparam_dict = self._key_2_hyperparams_dict[key]
512
+ d_shared, d_individs = hyperparam_dict["d_shared"], hyperparam_dict["d_individs"]
513
+ stage, group = key
514
+ if idx == 0:
515
+ max_rank = d_shared
516
+ elif 1 <= idx < self.n_groups_ + 1:
517
+ if stage == 0:
518
+ max_rank = d_shared
519
+ else:
520
+ max_rank = d_individs[idx - 1]
521
+ else:
522
+ idx_in_group = np.sum(self.group_indices[:idx - self.n_groups_ - 1] == group)
523
+ max_rank = d_individs[idx_in_group]
524
+ return max_rank