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.
- GroupMultiNeSS/MASE.py +72 -0
- GroupMultiNeSS/__init__.py +7 -0
- GroupMultiNeSS/base.py +337 -0
- GroupMultiNeSS/data_generation.py +205 -0
- GroupMultiNeSS/group_multiness.py +524 -0
- GroupMultiNeSS/multiness.py +1307 -0
- GroupMultiNeSS/shared_space_hunting.py +315 -0
- GroupMultiNeSS/utils.py +379 -0
- groupmultiness-0.0.1.dist-info/METADATA +102 -0
- groupmultiness-0.0.1.dist-info/RECORD +13 -0
- groupmultiness-0.0.1.dist-info/WHEEL +5 -0
- groupmultiness-0.0.1.dist-info/licenses/LICENSE +22 -0
- groupmultiness-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1307 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import List, Dict, Callable, Union, Tuple
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
from functools import partial
|
|
5
|
+
import seaborn as sns
|
|
6
|
+
from multiprocessing import Manager
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from itertools import product
|
|
9
|
+
from utils import MultipleNetworkTrainTestSplitter
|
|
10
|
+
from more_itertools import zip_equal
|
|
11
|
+
|
|
12
|
+
from .utils import soft_thresholding_operator, hard_thresholding_operator, make_error_report, \
|
|
13
|
+
mean_frobenius_error, EarlyStopper, if_scalar_or_given_length_array, fill_diagonals
|
|
14
|
+
from .base import BaseMultiplexNetworksModel, BaseRefitting, SharedMemoryMatrixFitter
|
|
15
|
+
from .MASE import ASE
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class BaseMultiNeSS(BaseMultiplexNetworksModel, SharedMemoryMatrixFitter):
|
|
19
|
+
"""
|
|
20
|
+
Base class for implementing the MultiNeSS and GroupMultiNeSS models.
|
|
21
|
+
|
|
22
|
+
Inherits from
|
|
23
|
+
----------
|
|
24
|
+
BaseMultiplexNetworksModel
|
|
25
|
+
Provides core multiplex network modeling functionalities.
|
|
26
|
+
SharedMemoryMatrixFitter
|
|
27
|
+
Provides methods for fitting and managing shared memory matrices.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def get_all_fitted_matrices_by_type(self):
|
|
31
|
+
"""
|
|
32
|
+
Retrieve all fitted matrices, separated by shared and individual components.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
list of np.ndarray
|
|
37
|
+
A list containing:
|
|
38
|
+
- The shared latent space matrix.
|
|
39
|
+
- A list of individual latent space matrices.
|
|
40
|
+
"""
|
|
41
|
+
return [self.get_shared_latent_space(), self.get_individual_latent_spaces()]
|
|
42
|
+
|
|
43
|
+
def get_all_matrix_latent_positions(self, ds: List[int] = None, check_if_symmetric=True):
|
|
44
|
+
"""
|
|
45
|
+
Compute latent positions for all fitted matrices using adjacency spectral embedding (ASE).
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
ds : list of int, optional
|
|
50
|
+
Dimensions of embeddings for each matrix. If None, the matrix ranks are used.
|
|
51
|
+
check_if_symmetric : bool, default=True
|
|
52
|
+
Whether to check if matrices are symmetric before embedding.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
list of np.ndarray
|
|
57
|
+
List of latent position matrices for each fitted matrix, one per component.
|
|
58
|
+
"""
|
|
59
|
+
matrices = self.get_all_fitted_matrices()
|
|
60
|
+
if ds is None:
|
|
61
|
+
ds = [np.linalg.matrix_rank(matrix) for matrix in matrices]
|
|
62
|
+
return [ASE(matrix, d, check_if_symmetric=check_if_symmetric) if d != 0 else np.empty((self.n_nodes_, 0))
|
|
63
|
+
for matrix, d in zip_equal(matrices, ds)]
|
|
64
|
+
|
|
65
|
+
def get_node_latent_positions_across_layers(self):
|
|
66
|
+
"""
|
|
67
|
+
Compute node latent positions across all layers, concatenating shared and relevant individual embeddings.
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
list of np.ndarray
|
|
72
|
+
A list of latent position matrices, one for each layer. Each matrix has shape (n_nodes, total_dim).
|
|
73
|
+
"""
|
|
74
|
+
matrix_positions = self.get_all_matrix_latent_positions()
|
|
75
|
+
all_param_indices = np.arange(len(matrix_positions))
|
|
76
|
+
latent_positions = []
|
|
77
|
+
for idx in range(self.n_layers_):
|
|
78
|
+
layer_depend_indices = all_param_indices[self._param_participance_masks[:, idx]]
|
|
79
|
+
layer_pos = np.hstack([matrix_positions[param_idx] for param_idx in layer_depend_indices])
|
|
80
|
+
latent_positions.append(layer_pos)
|
|
81
|
+
return latent_positions
|
|
82
|
+
|
|
83
|
+
def get_shared_latent_space(self):
|
|
84
|
+
"""
|
|
85
|
+
Retrieve the shared latent space matrix.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
np.ndarray
|
|
90
|
+
The shared latent space matrix estimated across all layers.
|
|
91
|
+
"""
|
|
92
|
+
return self.get_all_fitted_matrices()[0]
|
|
93
|
+
|
|
94
|
+
def get_individual_latent_spaces(self):
|
|
95
|
+
"""
|
|
96
|
+
Retrieve the individual latent space matrices.
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
list of np.ndarray
|
|
101
|
+
List of latent space matrices specific to each network layer.
|
|
102
|
+
"""
|
|
103
|
+
return self.get_all_fitted_matrices()[1:]
|
|
104
|
+
|
|
105
|
+
def _get_param_participance_masks(self):
|
|
106
|
+
"""
|
|
107
|
+
Construct boolean masks indicating which parameters (shared or individual)
|
|
108
|
+
contribute to each network layer.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
np.ndarray of bool, shape (n_parameters, n_layers)
|
|
113
|
+
Participation mask where True indicates inclusion of a parameter in a layer.
|
|
114
|
+
"""
|
|
115
|
+
return np.stack([np.ones(self.n_layers_, dtype=bool), *np.eye(self.n_layers_, dtype=bool)])
|
|
116
|
+
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _get_all_fitted_parameter_names():
|
|
119
|
+
"""
|
|
120
|
+
Get the names of all fitted parameter types.
|
|
121
|
+
|
|
122
|
+
Returns
|
|
123
|
+
-------
|
|
124
|
+
list of str
|
|
125
|
+
Names of the model components: shared and individual.
|
|
126
|
+
"""
|
|
127
|
+
return ["Shared component", "Individual components"]
|
|
128
|
+
|
|
129
|
+
def compute_expected_adjacency(self):
|
|
130
|
+
"""
|
|
131
|
+
Compute the expected adjacency matrices from the fitted latent spaces.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
list of np.ndarray
|
|
136
|
+
Expected adjacency matrices obtained via the model link function.
|
|
137
|
+
"""
|
|
138
|
+
return self.link_(self.compute_latent_spaces())
|
|
139
|
+
|
|
140
|
+
def compute_latent_spaces(self):
|
|
141
|
+
"""
|
|
142
|
+
Compute latent spaces combining shared and individual components,
|
|
143
|
+
optionally zeroing diagonals if loops are not allowed.
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
list of np.ndarray
|
|
148
|
+
Latent space matrices adjusted for model constraints.
|
|
149
|
+
"""
|
|
150
|
+
ls = self.get_shared_latent_space() + self.get_individual_latent_spaces()
|
|
151
|
+
if not self.loops_allowed:
|
|
152
|
+
fill_diagonals(ls, val=0., inplace=True)
|
|
153
|
+
return ls
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class BaseMultiNeSSRefined(BaseMultiNeSS, BaseRefitting):
|
|
157
|
+
"""
|
|
158
|
+
Refined version of the BaseMultiNeSS model with optimization, refitting, and
|
|
159
|
+
training history management. Implements parameter optimization loops,
|
|
160
|
+
hyperparameter validation, and history tracking during training.
|
|
161
|
+
|
|
162
|
+
Inherits from
|
|
163
|
+
----------
|
|
164
|
+
BaseMultiNeSS
|
|
165
|
+
Provides multiplex latent space modeling functionality.
|
|
166
|
+
BaseRefitting
|
|
167
|
+
Adds post-optimization refitting procedures.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(self, edge_distrib: str = "normal", loops_allowed: bool = True,
|
|
171
|
+
max_rank: int = None, init_rank: int = None,
|
|
172
|
+
sigmas: Union[float, List, np.ndarray] = None):
|
|
173
|
+
"""
|
|
174
|
+
Initialize a refined MultiNeSS model with specified configuration.
|
|
175
|
+
|
|
176
|
+
Parameters
|
|
177
|
+
----------
|
|
178
|
+
edge_distrib : {'normal', 'bernoulli'}, default='normal'
|
|
179
|
+
Distributional assumption for edge weights.
|
|
180
|
+
loops_allowed : bool, default=True
|
|
181
|
+
Whether self-loops are allowed in adjacency matrices.
|
|
182
|
+
max_rank : int, optional
|
|
183
|
+
Maximum rank allowed during optimization.
|
|
184
|
+
init_rank : int, optional
|
|
185
|
+
Initial rank for spectral thresholding.
|
|
186
|
+
sigmas : float or array-like, optional
|
|
187
|
+
Edge variance(s) or standard deviations for Gaussian edges.
|
|
188
|
+
"""
|
|
189
|
+
super().__init__(edge_distrib=edge_distrib, loops_allowed=loops_allowed)
|
|
190
|
+
self.max_rank = max_rank
|
|
191
|
+
self.init_rank = init_rank
|
|
192
|
+
self.sigmas = sigmas
|
|
193
|
+
|
|
194
|
+
def _optimized_params_init(self, As, key: Tuple[int, int]) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Initialize model parameters before optimization via rank-thresholded averaging.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
As : np.ndarray
|
|
201
|
+
Input observed adjacency tensors of shape (n_layers, n_nodes, n_nodes).
|
|
202
|
+
key : tuple of int
|
|
203
|
+
Key identifying optimization stage.
|
|
204
|
+
"""
|
|
205
|
+
param_indices = self._key_2_param_indices[key]
|
|
206
|
+
param_inits = []
|
|
207
|
+
for idx in param_indices:
|
|
208
|
+
mask = self._param_participance_masks[idx]
|
|
209
|
+
param_init = hard_thresholding_operator(np.nansum(As[mask], axis=0) / mask.sum(), max_rank=self.init_rank)
|
|
210
|
+
param_inits.append(param_init)
|
|
211
|
+
self._set_matrices(param_inits, indices=param_indices)
|
|
212
|
+
|
|
213
|
+
def _update_history(self, nll: float, key: Tuple[int, int],
|
|
214
|
+
save_nll_history: bool = True,
|
|
215
|
+
save_latent_history: bool = False) -> None:
|
|
216
|
+
"""
|
|
217
|
+
Update optimization history (NLL and optionally latent states).
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
nll : float
|
|
222
|
+
Current negative log-likelihood value.
|
|
223
|
+
key : tuple of int
|
|
224
|
+
Identifier for optimization stage.
|
|
225
|
+
save_nll_history : bool, default=True
|
|
226
|
+
Whether to save NLL history.
|
|
227
|
+
save_latent_history : bool, default=False
|
|
228
|
+
Whether to save latent variable history.
|
|
229
|
+
"""
|
|
230
|
+
if save_nll_history:
|
|
231
|
+
self._update_nll_history(nll=nll, key=key)
|
|
232
|
+
if save_latent_history:
|
|
233
|
+
self._update_latent_history(key)
|
|
234
|
+
|
|
235
|
+
def _reset_history(self, key: Tuple[int, int]):
|
|
236
|
+
"""
|
|
237
|
+
Reset optimization history for a given key.
|
|
238
|
+
|
|
239
|
+
Parameters
|
|
240
|
+
----------
|
|
241
|
+
key : tuple of int
|
|
242
|
+
Identifier for optimization stage.
|
|
243
|
+
"""
|
|
244
|
+
hist_key = self._key_2_history_key[key]
|
|
245
|
+
self.nll_history_[hist_key][:] = []
|
|
246
|
+
|
|
247
|
+
def _update_nll_history(self, nll: float, key: Tuple[int, int]):
|
|
248
|
+
"""
|
|
249
|
+
Append new negative log-likelihood value to history.
|
|
250
|
+
|
|
251
|
+
Parameters
|
|
252
|
+
----------
|
|
253
|
+
nll : float
|
|
254
|
+
Negative log-likelihood value.
|
|
255
|
+
key : tuple of int
|
|
256
|
+
Identifier for optimization stage.
|
|
257
|
+
"""
|
|
258
|
+
history_key = self._key_2_history_key[key]
|
|
259
|
+
self.nll_history_[history_key].append(nll)
|
|
260
|
+
|
|
261
|
+
def _update_latent_history(self, key: Tuple[int, int]):
|
|
262
|
+
"""
|
|
263
|
+
Append current latent space matrices to history.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
key : tuple of int
|
|
268
|
+
Identifier for optimization stage.
|
|
269
|
+
"""
|
|
270
|
+
history_key = self._key_2_history_key[key]
|
|
271
|
+
self.latent_history_[history_key].append(deepcopy(self.get_all_fitted_matrices_by_type()))
|
|
272
|
+
|
|
273
|
+
def _make_key_2_history_key_dict(self) -> None:
|
|
274
|
+
"""Create mapping from optimization keys to history keys."""
|
|
275
|
+
self._key_2_history_key = {(0, 0): "First Stage"}
|
|
276
|
+
|
|
277
|
+
def _make_key_2_param_indices_dict(self) -> None:
|
|
278
|
+
"""Create mapping from optimization keys to parameter index lists."""
|
|
279
|
+
self._key_2_param_indices = {(0, 0): list(range(self.n_fitted_matrices))}
|
|
280
|
+
|
|
281
|
+
def _make_key_2_hyperparams_dict(self) -> None:
|
|
282
|
+
"""Initialize empty dictionary for hyperparameters per optimization key."""
|
|
283
|
+
self._key_2_hyperparams_dict = dict()
|
|
284
|
+
|
|
285
|
+
def _init_keys(self):
|
|
286
|
+
"""Initialize all internal key mapping dictionaries."""
|
|
287
|
+
self._make_key_2_history_key_dict()
|
|
288
|
+
self._make_key_2_param_indices_dict()
|
|
289
|
+
self._make_key_2_hyperparams_dict()
|
|
290
|
+
|
|
291
|
+
def _set_hyperparams(self, params_dict: Dict[str, float], key: Tuple[int, int]):
|
|
292
|
+
"""
|
|
293
|
+
Set hyperparameter values for the given key.
|
|
294
|
+
|
|
295
|
+
Parameters
|
|
296
|
+
----------
|
|
297
|
+
params_dict : dict
|
|
298
|
+
Dictionary mapping hyperparameter names to their values.
|
|
299
|
+
key : tuple of int
|
|
300
|
+
Optimization stage identifier.
|
|
301
|
+
"""
|
|
302
|
+
for param_name, param_val in params_dict.items():
|
|
303
|
+
old_val = self._key_2_hyperparams_dict[key][param_name]
|
|
304
|
+
param_val = param_val if np.isscalar(old_val) else param_val * np.ones_like(old_val)
|
|
305
|
+
self._key_2_hyperparams_dict[key][param_name] = param_val
|
|
306
|
+
|
|
307
|
+
def _init_history(self, manager: Manager):
|
|
308
|
+
"""
|
|
309
|
+
Initialize shared memory-based history tracking structures.
|
|
310
|
+
|
|
311
|
+
Parameters
|
|
312
|
+
----------
|
|
313
|
+
manager : multiprocessing.Manager
|
|
314
|
+
Shared memory manager for inter-process synchronization.
|
|
315
|
+
"""
|
|
316
|
+
self._shm_lock = manager.Lock()
|
|
317
|
+
self.optim_info = manager.dict({'n_svd_calls': 0})
|
|
318
|
+
self.nll_history_ = manager.dict({hist_key: manager.list() for hist_key in self._key_2_history_key.values()})
|
|
319
|
+
self.latent_history_ = manager.dict({hist_key: manager.list() for hist_key in self._key_2_history_key.values()})
|
|
320
|
+
|
|
321
|
+
def _validate_hyperparams(self):
|
|
322
|
+
"""
|
|
323
|
+
Validate and initialize hyperparameters such as ranks and sigma scales.
|
|
324
|
+
|
|
325
|
+
Raises
|
|
326
|
+
------
|
|
327
|
+
AssertionError
|
|
328
|
+
If initial or maximum rank constraints are violated.
|
|
329
|
+
NotImplementedError
|
|
330
|
+
If sigmas dimensionality is unsupported.
|
|
331
|
+
"""
|
|
332
|
+
if self.init_rank is None:
|
|
333
|
+
self.init_rank = 1.
|
|
334
|
+
|
|
335
|
+
assert self.n_nodes_ >= self.init_rank, "Initial rank should not be greater than the number of nodes"
|
|
336
|
+
if self.max_rank is not None:
|
|
337
|
+
assert self.max_rank >= self.init_rank, "Initial rank should not be greater than maximum rank"
|
|
338
|
+
else:
|
|
339
|
+
self.max_rank = int(np.sqrt(self.n_nodes_))
|
|
340
|
+
|
|
341
|
+
if self.sigmas is None:
|
|
342
|
+
self.sigmas_ = np.ones((self.n_layers_, self.n_nodes_, self.n_nodes_), dtype=float)
|
|
343
|
+
else:
|
|
344
|
+
self.sigmas_ = np.array(self.sigmas, dtype=float)
|
|
345
|
+
|
|
346
|
+
if self.sigmas_.ndim == 0:
|
|
347
|
+
self.sigmas_ = self.sigmas_ * np.ones((self.n_layers_, self.n_nodes_, self.n_nodes_), dtype=float)
|
|
348
|
+
elif self.sigmas_.ndim == 1:
|
|
349
|
+
self.sigmas_ = self.sigmas_[:, None, None] * np.ones((1, self.n_nodes_, self.n_nodes_))
|
|
350
|
+
elif self.sigmas_.ndim == 2:
|
|
351
|
+
assert self.sigmas_.shape == (self.n_nodes_, self.n_nodes_)
|
|
352
|
+
self.sigmas_ = np.stack([self.sigmas_] * self.n_layers_)
|
|
353
|
+
elif self.sigmas_.ndim == 3:
|
|
354
|
+
assert self.sigmas_.shape == (self.n_layers_, self.n_nodes_, self.n_nodes_)
|
|
355
|
+
else:
|
|
356
|
+
raise NotImplementedError("sigmas should be a scalar, 1d, 2d, or 3d array.")
|
|
357
|
+
|
|
358
|
+
def _compute_projected_map(self, idx: int, lr: float, key: Tuple[int, int]) -> Callable:
|
|
359
|
+
"""Placeholder for projected map computation (to be implemented in subclass)."""
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
def _update_matrix(self, idx, lr: float, sample_grads: np.array, key: Tuple[int, int]):
|
|
363
|
+
"""
|
|
364
|
+
Update a single parameter matrix via gradient step and projection.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
idx : int
|
|
369
|
+
Index of the parameter matrix to update.
|
|
370
|
+
lr : float
|
|
371
|
+
Learning rate.
|
|
372
|
+
sample_grads : np.ndarray
|
|
373
|
+
Gradient tensor of the same shape as observed data.
|
|
374
|
+
key : tuple of int
|
|
375
|
+
Optimization stage identifier.
|
|
376
|
+
"""
|
|
377
|
+
projected_map = self._compute_projected_map(idx, lr=lr, key=key)
|
|
378
|
+
mask = self._param_participance_masks[idx]
|
|
379
|
+
matrix_grad = np.nansum(sample_grads[mask], axis=0)
|
|
380
|
+
cur_matrix = self.get_all_fitted_matrices()[idx]
|
|
381
|
+
upd_matrix = projected_map(cur_matrix - lr / mask.sum() * matrix_grad)
|
|
382
|
+
self._set_matrix(upd_matrix, idx)
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def n_fitted_matrices(self) -> int:
|
|
386
|
+
"""int: Total number of fitted matrices (shared + individual)."""
|
|
387
|
+
return 1 + self.n_layers_
|
|
388
|
+
|
|
389
|
+
def _pre_fit(self):
|
|
390
|
+
"""Perform pre-fitting initialization including parameters, keys, and history."""
|
|
391
|
+
self._init_param_matrices(np.zeros((self.n_fitted_matrices, self.n_nodes_, self.n_nodes_)))
|
|
392
|
+
self._validate_hyperparams()
|
|
393
|
+
self._init_keys()
|
|
394
|
+
self._init_history(manager=Manager())
|
|
395
|
+
|
|
396
|
+
def _optimization_step(self, param_indices, key: Tuple[int, int], As, lr: float, iteration: int,
|
|
397
|
+
verbose: bool, tol: float,
|
|
398
|
+
save_latent_history: bool, save_nll_history: bool) -> float:
|
|
399
|
+
"""
|
|
400
|
+
Execute one optimization iteration over a subset of parameters.
|
|
401
|
+
|
|
402
|
+
Parameters
|
|
403
|
+
----------
|
|
404
|
+
param_indices : list of int
|
|
405
|
+
Indices of parameters to optimize.
|
|
406
|
+
key : tuple of int
|
|
407
|
+
Optimization stage identifier.
|
|
408
|
+
As : np.ndarray
|
|
409
|
+
Input adjacency tensors.
|
|
410
|
+
lr : float
|
|
411
|
+
Learning rate.
|
|
412
|
+
iteration : int
|
|
413
|
+
Current iteration number.
|
|
414
|
+
verbose : bool
|
|
415
|
+
Whether to print progress.
|
|
416
|
+
tol : float
|
|
417
|
+
Tolerance for rounding output.
|
|
418
|
+
save_latent_history : bool
|
|
419
|
+
Whether to store latent variable history.
|
|
420
|
+
save_nll_history : bool
|
|
421
|
+
Whether to store NLL history.
|
|
422
|
+
|
|
423
|
+
Returns
|
|
424
|
+
-------
|
|
425
|
+
float
|
|
426
|
+
Negative log-likelihood after the update step.
|
|
427
|
+
"""
|
|
428
|
+
for idx in param_indices:
|
|
429
|
+
sample_grads = self._nll_sample_grads(As)
|
|
430
|
+
self._update_matrix(idx, lr=lr, sample_grads=sample_grads, key=key)
|
|
431
|
+
|
|
432
|
+
obs_mask = self._param_participance_masks[param_indices].any(0)
|
|
433
|
+
nll = self._compute_nll(As, obs_mask=obs_mask)
|
|
434
|
+
|
|
435
|
+
with self._shm_lock:
|
|
436
|
+
self._update_history(nll, key=key,
|
|
437
|
+
save_latent_history=save_latent_history, save_nll_history=save_nll_history)
|
|
438
|
+
|
|
439
|
+
if verbose:
|
|
440
|
+
nll = np.round(nll, 1 - int(np.log10(tol))) if tol > 0 else nll
|
|
441
|
+
print(f"{self._key_2_history_key[key]}, Iteration {iteration}, NLL {nll}")
|
|
442
|
+
return nll
|
|
443
|
+
|
|
444
|
+
def _optimize_params(self, key: Tuple[int, int], As, lr: float,
|
|
445
|
+
max_iter: int, tol: float, patience,
|
|
446
|
+
verbose: bool, verbose_interval: int,
|
|
447
|
+
save_latent_history: bool, save_nll_history: bool,
|
|
448
|
+
refit: bool):
|
|
449
|
+
"""
|
|
450
|
+
Optimize model parameters via iterative updates and early stopping.
|
|
451
|
+
|
|
452
|
+
Parameters
|
|
453
|
+
----------
|
|
454
|
+
key : tuple of int
|
|
455
|
+
Optimization stage identifier.
|
|
456
|
+
As : np.ndarray
|
|
457
|
+
Observed adjacency tensors.
|
|
458
|
+
lr : float
|
|
459
|
+
Learning rate.
|
|
460
|
+
max_iter : int
|
|
461
|
+
Maximum number of iterations.
|
|
462
|
+
tol : float
|
|
463
|
+
Stopping tolerance for early stopping.
|
|
464
|
+
patience : int
|
|
465
|
+
Number of iterations with no improvement before stopping.
|
|
466
|
+
verbose : bool
|
|
467
|
+
Whether to print progress.
|
|
468
|
+
verbose_interval : int
|
|
469
|
+
Interval (in iterations) for printing updates.
|
|
470
|
+
save_latent_history : bool
|
|
471
|
+
Whether to save latent variables at each step.
|
|
472
|
+
save_nll_history : bool
|
|
473
|
+
Whether to save NLL values.
|
|
474
|
+
refit : bool
|
|
475
|
+
Whether to refit parameters after optimization.
|
|
476
|
+
"""
|
|
477
|
+
self._reset_history(key=key)
|
|
478
|
+
param_indices = self._key_2_param_indices[key]
|
|
479
|
+
self._optimized_params_init(As, key=key)
|
|
480
|
+
early_stopper = EarlyStopper(patience=patience, higher_better=False, rtol=tol)
|
|
481
|
+
for iteration in range(max_iter):
|
|
482
|
+
nll = self._optimization_step(param_indices, key=key, As=As, lr=lr, iteration=iteration, tol=tol,
|
|
483
|
+
verbose=verbose and iteration % verbose_interval == 0,
|
|
484
|
+
save_latent_history=save_latent_history, save_nll_history=save_nll_history)
|
|
485
|
+
|
|
486
|
+
if early_stopper.stop_check(nll):
|
|
487
|
+
with self._shm_lock:
|
|
488
|
+
self.optim_info["n_svd_calls"] += (iteration + 1) * len(param_indices)
|
|
489
|
+
if verbose:
|
|
490
|
+
print(f"Early stop activated on iteration {iteration}!\n")
|
|
491
|
+
break
|
|
492
|
+
|
|
493
|
+
if refit:
|
|
494
|
+
self.refit(As, refit_matrix_indices=param_indices)
|
|
495
|
+
|
|
496
|
+
def _fit(self, As, **optim_kwargs):
|
|
497
|
+
"""
|
|
498
|
+
Full model fitting routine combining initialization and optimization.
|
|
499
|
+
|
|
500
|
+
Parameters
|
|
501
|
+
----------
|
|
502
|
+
As : np.ndarray
|
|
503
|
+
Input adjacency tensors.
|
|
504
|
+
**optim_kwargs
|
|
505
|
+
Optimization keyword arguments passed to `_optimize_params`.
|
|
506
|
+
"""
|
|
507
|
+
self._pre_fit()
|
|
508
|
+
self._optimize_params(As=As, key=(0, 0), **optim_kwargs)
|
|
509
|
+
|
|
510
|
+
def _nll_sample_grads(self, As):
|
|
511
|
+
"""
|
|
512
|
+
Compute sample gradients for the negative log-likelihood objective.
|
|
513
|
+
|
|
514
|
+
Parameters
|
|
515
|
+
----------
|
|
516
|
+
As : np.ndarray
|
|
517
|
+
Observed adjacency matrices.
|
|
518
|
+
|
|
519
|
+
Returns
|
|
520
|
+
-------
|
|
521
|
+
np.ndarray
|
|
522
|
+
Gradient tensor for each layer.
|
|
523
|
+
"""
|
|
524
|
+
resids = self.compute_expected_adjacency() - As
|
|
525
|
+
if self.edge_distrib == "normal":
|
|
526
|
+
resids /= self.sigmas_ ** 2
|
|
527
|
+
return resids
|
|
528
|
+
|
|
529
|
+
def _compute_nll(self, As, obs_mask=None, eps=1e-8):
|
|
530
|
+
"""
|
|
531
|
+
Compute negative log-likelihood for observed data under the model.
|
|
532
|
+
|
|
533
|
+
Parameters
|
|
534
|
+
----------
|
|
535
|
+
As : np.ndarray
|
|
536
|
+
Observed adjacency matrices.
|
|
537
|
+
obs_mask : np.ndarray, optional
|
|
538
|
+
Boolean mask for observed layers.
|
|
539
|
+
eps : float, default=1e-8
|
|
540
|
+
Small constant for numerical stability.
|
|
541
|
+
|
|
542
|
+
Returns
|
|
543
|
+
-------
|
|
544
|
+
float
|
|
545
|
+
Negative log-likelihood value.
|
|
546
|
+
"""
|
|
547
|
+
if obs_mask is None:
|
|
548
|
+
obs_mask = np.ones(self.n_layers_, dtype=bool)
|
|
549
|
+
triu_mask = np.triu(np.ones((self.n_nodes_, self.n_nodes_)), k=0 if self.loops_allowed else 1)
|
|
550
|
+
Ps = self.compute_expected_adjacency()
|
|
551
|
+
if self.edge_distrib == "normal":
|
|
552
|
+
full_nll = (As - Ps) ** 2 / (2. * self.sigmas_ ** 2)
|
|
553
|
+
else:
|
|
554
|
+
Ps = np.clip(Ps, eps, 1. - eps)
|
|
555
|
+
full_nll = -As * np.log(Ps) - (1. - As) * np.log(1. - Ps)
|
|
556
|
+
return np.nansum(full_nll[obs_mask] * triu_mask)
|
|
557
|
+
|
|
558
|
+
def make_final_error_report(self, *true_params, Ps=None, relative_errors=True, round_digits: int = 3):
|
|
559
|
+
"""
|
|
560
|
+
Compute final error report comparing estimated and true parameters.
|
|
561
|
+
|
|
562
|
+
Parameters
|
|
563
|
+
----------
|
|
564
|
+
*true_params : np.ndarray
|
|
565
|
+
Ground-truth parameter matrices (shared, individual, etc.).
|
|
566
|
+
Ps : np.ndarray, optional
|
|
567
|
+
True expected adjacency matrices.
|
|
568
|
+
relative_errors : bool, default=True
|
|
569
|
+
Whether to compute relative Frobenius errors.
|
|
570
|
+
round_digits : int, default=3
|
|
571
|
+
Number of digits to round results.
|
|
572
|
+
|
|
573
|
+
Returns
|
|
574
|
+
-------
|
|
575
|
+
dict
|
|
576
|
+
Mapping of parameter names to error metrics.
|
|
577
|
+
"""
|
|
578
|
+
estimate_params = self.get_all_fitted_matrices_by_type()
|
|
579
|
+
param_names = self._get_all_fitted_parameter_names()
|
|
580
|
+
assert len(true_params) == len(estimate_params)
|
|
581
|
+
assert all([param.shape == pred_param.shape for param, pred_param in zip(true_params, estimate_params)])
|
|
582
|
+
if Ps is not None:
|
|
583
|
+
true_params = [*true_params, Ps]
|
|
584
|
+
estimate_params.append(self.compute_expected_adjacency())
|
|
585
|
+
param_names.append("Ps")
|
|
586
|
+
return {param_name: round(mean_frobenius_error(true_param, pred_param,
|
|
587
|
+
relative=relative_errors, include_offdiag=self.loops_allowed),
|
|
588
|
+
round_digits)
|
|
589
|
+
for param_name, true_param, pred_param in zip(param_names, true_params, estimate_params)}
|
|
590
|
+
|
|
591
|
+
def get_error_history(self, *true_params, relative_errors=True):
|
|
592
|
+
"""
|
|
593
|
+
Retrieve history of parameter estimation errors across iterations.
|
|
594
|
+
|
|
595
|
+
Parameters
|
|
596
|
+
----------
|
|
597
|
+
true_params : tuple[np.array]
|
|
598
|
+
Ground truth parameters.
|
|
599
|
+
relative_errors : bool, default=True
|
|
600
|
+
Whether to compute relative errors.
|
|
601
|
+
|
|
602
|
+
Returns
|
|
603
|
+
-------
|
|
604
|
+
list of dict
|
|
605
|
+
Error values for each iteration.
|
|
606
|
+
"""
|
|
607
|
+
assert self.latent_history_, "Set save_latent_history == True during fit to access latent_history!"
|
|
608
|
+
assert len(true_params) == len(self._get_all_fitted_parameter_names()), \
|
|
609
|
+
f"Length of true_params should be equal to {len(self._get_all_fitted_parameter_names())}"
|
|
610
|
+
err_hist = []
|
|
611
|
+
for estimate_params in self.latent_history_:
|
|
612
|
+
iteration_errs = make_error_report(true_params, estimate_params, relative_errors=relative_errors)
|
|
613
|
+
err_hist.append(iteration_errs)
|
|
614
|
+
return err_hist
|
|
615
|
+
|
|
616
|
+
def plot_nll_history(self, n_cols: int = 4, fontsize: int = 14, figsize=(12, 20)):
|
|
617
|
+
"""
|
|
618
|
+
Plot negative log-likelihood history across optimization stages.
|
|
619
|
+
|
|
620
|
+
Parameters
|
|
621
|
+
----------
|
|
622
|
+
n_cols : int, default=4
|
|
623
|
+
Number of subplot columns.
|
|
624
|
+
fontsize : int, default=14
|
|
625
|
+
Font size for labels and titles.
|
|
626
|
+
figsize : tuple, default=(12, 20)
|
|
627
|
+
Figure size.
|
|
628
|
+
"""
|
|
629
|
+
n_cols = min(n_cols, len(self.nll_history_))
|
|
630
|
+
n_rows = int(np.ceil(len(self.nll_history_) / n_cols))
|
|
631
|
+
fig, axs = plt.subplots(ncols=n_cols, nrows=n_rows, figsize=figsize)
|
|
632
|
+
axs = np.array(axs).flatten()
|
|
633
|
+
for ax, (stage, nll_hist) in zip(axs, self.nll_history_.items()):
|
|
634
|
+
ax.plot(nll_hist)
|
|
635
|
+
ax.set_xlabel("Iteration", fontsize=fontsize)
|
|
636
|
+
ax.set_ylabel("Negative Loglikelihood", fontsize=fontsize)
|
|
637
|
+
ax.set_title(f"{stage}", fontsize=fontsize)
|
|
638
|
+
ax.tick_params(axis='both', labelsize=fontsize)
|
|
639
|
+
for idx in range(len(self.nll_history_), n_cols * n_rows):
|
|
640
|
+
fig.delaxes(axs[idx]) # Remove extra axes
|
|
641
|
+
plt.tight_layout()
|
|
642
|
+
plt.show()
|
|
643
|
+
|
|
644
|
+
def plot_error_history(self, *true_params, relative_errors=True, labels=None, fontsize=14,
|
|
645
|
+
print_final_errors=True):
|
|
646
|
+
"""
|
|
647
|
+
Plot evolution of parameter estimation errors over iterations.
|
|
648
|
+
|
|
649
|
+
Parameters
|
|
650
|
+
----------
|
|
651
|
+
*true_params : list of np.ndarray
|
|
652
|
+
Ground truth parameters.
|
|
653
|
+
relative_errors : bool, default=True
|
|
654
|
+
Whether to plot relative errors.
|
|
655
|
+
labels : list of str, optional
|
|
656
|
+
Custom labels for parameters.
|
|
657
|
+
fontsize : int, default=14
|
|
658
|
+
Font size for plot.
|
|
659
|
+
print_final_errors : bool, default=True
|
|
660
|
+
Whether to print final iteration errors.
|
|
661
|
+
"""
|
|
662
|
+
if labels is None:
|
|
663
|
+
labels = self._get_all_fitted_parameter_names()
|
|
664
|
+
else:
|
|
665
|
+
assert len(true_params) == len(labels), \
|
|
666
|
+
"labels should be of the same length as the length of true_params"
|
|
667
|
+
|
|
668
|
+
err_hist = self.get_error_history(*true_params, relative_errors=relative_errors)
|
|
669
|
+
if print_final_errors:
|
|
670
|
+
print("Final errors:", np.round(err_hist[-1], 2))
|
|
671
|
+
plt.figure(figsize=(12, 8))
|
|
672
|
+
plt.plot(err_hist, label=labels)
|
|
673
|
+
plt.legend(fontsize=fontsize)
|
|
674
|
+
plt.xlabel("Iteration", fontsize=fontsize)
|
|
675
|
+
plt.ylabel("Error", fontsize=fontsize)
|
|
676
|
+
plt.title("Relative Frobenius Errors", fontsize=fontsize)
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
class BaseMultiNeSSRefinedCV(BaseMultiNeSSRefined):
|
|
680
|
+
"""
|
|
681
|
+
Cross-validated refinement class for MultiNeSS models.
|
|
682
|
+
|
|
683
|
+
This class extends `BaseMultiNeSSRefined` by adding cross-validation (CV) procedures
|
|
684
|
+
for hyperparameter selection. It supports k-fold CV or a single hold-out test split,
|
|
685
|
+
and selects optimal parameters based on the mean negative log-likelihood (NLL).
|
|
686
|
+
|
|
687
|
+
Parameters
|
|
688
|
+
----------
|
|
689
|
+
param_grid : dict of {str: list}, optional
|
|
690
|
+
Dictionary defining the hyperparameter grid to explore.
|
|
691
|
+
cv_folds : int, optional
|
|
692
|
+
Number of cross-validation folds. If None, a single train/test split is used.
|
|
693
|
+
test_prop : float, default=0.1
|
|
694
|
+
Proportion of data used for the test set in a single split.
|
|
695
|
+
fix_layer_split : bool, default=False
|
|
696
|
+
Whether to fix the split of layers across folds.
|
|
697
|
+
use_1se_rule : bool, default=False
|
|
698
|
+
If True, selects the most parsimonious model within one standard error of the best mean NLL.
|
|
699
|
+
random_seed : int, optional
|
|
700
|
+
Random seed for reproducibility.
|
|
701
|
+
"""
|
|
702
|
+
|
|
703
|
+
def __init__(self, param_grid: Dict[str, List] = None, cv_folds=None, test_prop=0.1,
|
|
704
|
+
fix_layer_split: bool = False, use_1se_rule=False, random_seed: int = None, **kwargs):
|
|
705
|
+
super().__init__(**kwargs)
|
|
706
|
+
self.cv_folds = cv_folds
|
|
707
|
+
self.test_prop = test_prop
|
|
708
|
+
if param_grid is None:
|
|
709
|
+
self.param_grid = param_grid
|
|
710
|
+
else:
|
|
711
|
+
self.param_grid = dict(sorted(param_grid.items(), key=lambda kv: kv[0], reverse=True))
|
|
712
|
+
self.use_1se_rule = use_1se_rule
|
|
713
|
+
self.fix_layer_split = fix_layer_split
|
|
714
|
+
self.random_seed = random_seed
|
|
715
|
+
|
|
716
|
+
def _make_key_2_hyperparams_grid_dict(self) -> None:
|
|
717
|
+
"""
|
|
718
|
+
This method is typically called during setup to prepare
|
|
719
|
+
storage for layer-specific hyperparameter grids.
|
|
720
|
+
"""
|
|
721
|
+
self._key_2_hyperparams_grid = dict()
|
|
722
|
+
|
|
723
|
+
def _init_keys(self):
|
|
724
|
+
"""
|
|
725
|
+
Initialize model keys and corresponding hyperparameter grids.
|
|
726
|
+
|
|
727
|
+
Calls the parent `_init_keys` and then creates the
|
|
728
|
+
`_key_2_hyperparams_grid` dictionary.
|
|
729
|
+
"""
|
|
730
|
+
super()._init_keys()
|
|
731
|
+
self._make_key_2_hyperparams_grid_dict()
|
|
732
|
+
|
|
733
|
+
def _init_history(self, manager: Manager):
|
|
734
|
+
"""
|
|
735
|
+
Initialize multiprocessing-safe containers for cross-validation results.
|
|
736
|
+
|
|
737
|
+
Parameters
|
|
738
|
+
----------
|
|
739
|
+
manager : multiprocessing.Manager
|
|
740
|
+
Shared-memory manager used to create thread-safe dictionaries and lists.
|
|
741
|
+
"""
|
|
742
|
+
super()._init_history(manager)
|
|
743
|
+
self._cv_results = manager.dict(
|
|
744
|
+
{key: manager.dict({params: manager.list() for params in product(*param_grid_dict.values())})
|
|
745
|
+
for key, param_grid_dict in self._key_2_hyperparams_grid.items()}
|
|
746
|
+
)
|
|
747
|
+
self._best_params_dict = manager.dict()
|
|
748
|
+
|
|
749
|
+
def _set_best_hyperparams(self, key: Tuple[int, int]) -> None:
|
|
750
|
+
"""
|
|
751
|
+
Determine and store the best hyperparameters for a given key.
|
|
752
|
+
|
|
753
|
+
Parameters
|
|
754
|
+
----------
|
|
755
|
+
key : tuple of int
|
|
756
|
+
Identifier for the network-layer combination being optimized.
|
|
757
|
+
"""
|
|
758
|
+
res_dict = self._cv_results[key]
|
|
759
|
+
params, param_nlls = np.stack(list(res_dict.keys())), np.stack(list(res_dict.values()))
|
|
760
|
+
optim_nll_idx = np.argmin(param_nlls.mean(1))
|
|
761
|
+
if self.use_1se_rule and self.cv_folds > 1:
|
|
762
|
+
optim_param_nll_std = param_nlls.std(1, ddof=1)[optim_nll_idx]
|
|
763
|
+
param_nll_means = param_nlls.mean(1)
|
|
764
|
+
param_nll_1se_ub = param_nll_means[optim_nll_idx] + optim_param_nll_std / np.sqrt(self.cv_folds)
|
|
765
|
+
best_params = max(params[param_nll_means <= param_nll_1se_ub].tolist())
|
|
766
|
+
else:
|
|
767
|
+
best_params = params[optim_nll_idx]
|
|
768
|
+
|
|
769
|
+
with self._shm_lock:
|
|
770
|
+
self._best_params_dict[key] = dict(zip(self._key_2_hyperparams_grid[key].keys(), best_params))
|
|
771
|
+
self._set_hyperparams(params_dict=self._best_params_dict[key], key=key)
|
|
772
|
+
|
|
773
|
+
def _cv_hyperparams(self, As, fit_fun: Callable, comp_nll_fun: Callable, key: Tuple[int, int],
|
|
774
|
+
verbose=False):
|
|
775
|
+
"""
|
|
776
|
+
Perform cross-validation for a given key and hyperparameter grid.
|
|
777
|
+
|
|
778
|
+
Parameters
|
|
779
|
+
----------
|
|
780
|
+
As : list or np.ndarray
|
|
781
|
+
Input adjacency matrices or network representations.
|
|
782
|
+
fit_fun : callable
|
|
783
|
+
Function to fit the model on the training data.
|
|
784
|
+
comp_nll_fun : callable
|
|
785
|
+
Function to compute the negative log-likelihood on the test data.
|
|
786
|
+
key : tuple of int
|
|
787
|
+
Identifier for the network-layer combination.
|
|
788
|
+
verbose : bool, default=False
|
|
789
|
+
Whether to print progress and intermediate results.
|
|
790
|
+
"""
|
|
791
|
+
param_grid_dict = self._key_2_hyperparams_grid[key]
|
|
792
|
+
param_name_tuples = list(param_grid_dict.keys())
|
|
793
|
+
|
|
794
|
+
tts = MultipleNetworkTrainTestSplitter(loops_allowed=self.loops_allowed,
|
|
795
|
+
fix_layer_split=self.fix_layer_split, random_seed=self.random_seed)
|
|
796
|
+
if self.cv_folds is None:
|
|
797
|
+
fold_gen = [tts.train_test_split(As, test_prop=self.test_prop)]
|
|
798
|
+
else:
|
|
799
|
+
fold_gen = tts.kfold_split(As, n_splits=self.cv_folds)
|
|
800
|
+
|
|
801
|
+
for fold, (As_train, As_test) in enumerate(fold_gen):
|
|
802
|
+
if verbose:
|
|
803
|
+
print(f"#### CV Fold {fold + 1} ####\n")
|
|
804
|
+
|
|
805
|
+
for params in product(*param_grid_dict.values()):
|
|
806
|
+
params_dict = dict(zip(param_name_tuples, params))
|
|
807
|
+
if verbose:
|
|
808
|
+
print(f"\nStart working on {params_dict}", end="\n\n")
|
|
809
|
+
self._set_hyperparams(params_dict=params_dict, key=key)
|
|
810
|
+
fit_fun(As=As_train)
|
|
811
|
+
nll = comp_nll_fun(As_test)
|
|
812
|
+
with self._shm_lock:
|
|
813
|
+
self._cv_results[key][params].append(nll)
|
|
814
|
+
|
|
815
|
+
self._set_best_hyperparams(key=key)
|
|
816
|
+
if verbose:
|
|
817
|
+
print("\n#### Fitting on full data with best parameters! ####", end="\n\n")
|
|
818
|
+
fit_fun(As=As)
|
|
819
|
+
|
|
820
|
+
def get_cv_results(self):
|
|
821
|
+
"""
|
|
822
|
+
Retrieve the stored cross-validation results.
|
|
823
|
+
|
|
824
|
+
Returns
|
|
825
|
+
-------
|
|
826
|
+
dict
|
|
827
|
+
Nested dictionary mapping each key to its parameter combinations and
|
|
828
|
+
the list of NLL values across folds.
|
|
829
|
+
"""
|
|
830
|
+
assert hasattr(self, "_cv_results"), "Model was not fitted!"
|
|
831
|
+
return {key: {param: list(nll_list) for param, nll_list in param_dict.items()}
|
|
832
|
+
for key, param_dict in self._cv_results.items()}
|
|
833
|
+
|
|
834
|
+
def plot_cv_results(self, log_params=True, fontsize=12):
|
|
835
|
+
"""
|
|
836
|
+
Plot the cross-validation results for all keys.
|
|
837
|
+
|
|
838
|
+
Parameters
|
|
839
|
+
----------
|
|
840
|
+
log_params : bool, default=True
|
|
841
|
+
Whether to display hyperparameter values on a log10 scale.
|
|
842
|
+
fontsize : int, default=12
|
|
843
|
+
Font size for plot labels and titles.
|
|
844
|
+
"""
|
|
845
|
+
cv_res = sorted(self.get_cv_results().items(), key=lambda kv: kv[0])
|
|
846
|
+
fig, axs = plt.subplots(1, len(cv_res), figsize=(20, 6))
|
|
847
|
+
if len(cv_res) == 1:
|
|
848
|
+
axs = [axs]
|
|
849
|
+
for ax, (key, key_res) in zip(axs, cv_res):
|
|
850
|
+
hyperparam_2_grid = self._key_2_hyperparams_grid[key]
|
|
851
|
+
param_names = hyperparam_2_grid.keys()
|
|
852
|
+
ax.set_title(self._key_2_history_key[key], fontsize=fontsize + 3)
|
|
853
|
+
if len(param_names) == 1:
|
|
854
|
+
param_vals = np.hstack(list(key_res.keys()))
|
|
855
|
+
param_vals = np.log10(param_vals).round(2) if log_params else param_vals.round(2)
|
|
856
|
+
param_name = r'$\lambda$' if list(param_names)[0].startswith("lmbda") else r'$\alpha$'
|
|
857
|
+
cv_errs_over_folds = np.stack(list(key_res.values()))
|
|
858
|
+
for fold in range(cv_errs_over_folds.shape[1]):
|
|
859
|
+
ax.plot(param_vals, cv_errs_over_folds[:, fold], label=f"Fold={fold + 1}", marker="o")
|
|
860
|
+
ax.set_xticks(param_vals, param_vals, rotation=30, fontsize=fontsize)
|
|
861
|
+
ax.set_xlabel(r'$\log_{10}$' + f'({param_name})' if log_params else param_name, fontsize=fontsize + 3)
|
|
862
|
+
ax.set_ylabel("NLL", fontsize=fontsize + 3)
|
|
863
|
+
ax.legend(fontsize=fontsize)
|
|
864
|
+
else:
|
|
865
|
+
lmbda_grid, alpha_grid = hyperparam_2_grid.values()
|
|
866
|
+
heatmap_vals = [[np.mean(key_res[(lmbda, alpha)]) for lmbda in lmbda_grid] for alpha in alpha_grid]
|
|
867
|
+
best_lmbda, best_alpha = self.get_best_params_dict()[key].values()
|
|
868
|
+
col, row = list(lmbda_grid).index(best_lmbda), list(alpha_grid).index(best_alpha)
|
|
869
|
+
sns.heatmap(heatmap_vals, ax=ax)
|
|
870
|
+
ax.text(col + 0.5, row + 0.5, "X", c="red", fontsize=fontsize)
|
|
871
|
+
|
|
872
|
+
ax.set_xlabel(r'$\lambda$', fontsize=fontsize + 3)
|
|
873
|
+
ax.set_ylabel(r'$\alpha$', fontsize=fontsize + 3)
|
|
874
|
+
ax.set_xticklabels(lmbda_grid.round(2), fontsize=fontsize, rotation=30)
|
|
875
|
+
ax.set_yticklabels(alpha_grid.round(2), fontsize=fontsize)
|
|
876
|
+
plt.show()
|
|
877
|
+
|
|
878
|
+
def get_best_params_dict(self):
|
|
879
|
+
"""
|
|
880
|
+
Retrieve the best hyperparameters for each key.
|
|
881
|
+
|
|
882
|
+
Returns
|
|
883
|
+
-------
|
|
884
|
+
dict
|
|
885
|
+
Mapping from keys to the best parameter combinations selected by CV.
|
|
886
|
+
"""
|
|
887
|
+
assert hasattr(self, "_best_params_dict"), "Model was not fitted!"
|
|
888
|
+
return dict(self._best_params_dict)
|
|
889
|
+
|
|
890
|
+
def _optimize_params(self, key: Tuple[int, int], As, **optim_params):
|
|
891
|
+
"""
|
|
892
|
+
Optimize parameters for a given key, with optional cross-validation.
|
|
893
|
+
|
|
894
|
+
Parameters
|
|
895
|
+
----------
|
|
896
|
+
key : tuple of int
|
|
897
|
+
Identifier for the network-layer combination.
|
|
898
|
+
As : list or np.ndarray
|
|
899
|
+
Input adjacency matrices or network representations.
|
|
900
|
+
**optim_params : dict
|
|
901
|
+
Additional optimization parameters such as verbosity or stopping criteria.
|
|
902
|
+
"""
|
|
903
|
+
param_indices = self._key_2_param_indices[key]
|
|
904
|
+
obs_mask = self._param_participance_masks[param_indices].any(0)
|
|
905
|
+
comp_nll_fun = partial(super()._compute_nll, obs_mask=obs_mask)
|
|
906
|
+
fit_fun = partial(super()._optimize_params, key=key, **optim_params)
|
|
907
|
+
if self._key_2_hyperparams_grid[key]:
|
|
908
|
+
self._cv_hyperparams(As, fit_fun=fit_fun, comp_nll_fun=comp_nll_fun, key=key,
|
|
909
|
+
verbose=optim_params["verbose"])
|
|
910
|
+
else:
|
|
911
|
+
super()._optimize_params(key=key, As=As, **optim_params)
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
class MultiNeSS(BaseMultiNeSSRefined, BaseRefitting):
|
|
915
|
+
"""
|
|
916
|
+
This class extends `BaseMultiNeSSRefined` by adding refitting and fixing updates
|
|
917
|
+
as soft-thresholding with given parameters
|
|
918
|
+
|
|
919
|
+
Parameters
|
|
920
|
+
----------
|
|
921
|
+
lmbda : float, optional
|
|
922
|
+
Global penalty scaling factor.
|
|
923
|
+
alpha : float, list, or np.ndarray, optional
|
|
924
|
+
Layer-specific scaling factors for penalties.
|
|
925
|
+
max_rank : int, optional
|
|
926
|
+
Maximum allowed rank for low-rank projection.
|
|
927
|
+
init_rank : int, optional
|
|
928
|
+
Initial rank used for parameter initialization.
|
|
929
|
+
edge_distrib : str, default='normal'
|
|
930
|
+
Edge distribution assumed ('normal' or 'bernoulli').
|
|
931
|
+
sigmas : float, list, or np.ndarray, optional
|
|
932
|
+
Standard deviation(s) for edges when edge_distrib='normal'.
|
|
933
|
+
loops_allowed : bool, default=True
|
|
934
|
+
Whether self-loops are allowed in the network.
|
|
935
|
+
refit_threshold : float, default=1e-8
|
|
936
|
+
Threshold for triggering refit in BaseRefitting.
|
|
937
|
+
"""
|
|
938
|
+
|
|
939
|
+
def __init__(self,
|
|
940
|
+
lmbda: float = None,
|
|
941
|
+
alpha: Union[float, List, np.ndarray] = None,
|
|
942
|
+
max_rank: int = None, init_rank: int = None,
|
|
943
|
+
edge_distrib: str = "normal",
|
|
944
|
+
sigmas: Union[float, List, np.ndarray] = None,
|
|
945
|
+
loops_allowed: bool = True,
|
|
946
|
+
refit_threshold=1e-8):
|
|
947
|
+
|
|
948
|
+
BaseMultiNeSSRefined.__init__(self, edge_distrib=edge_distrib, max_rank=max_rank, init_rank=init_rank,
|
|
949
|
+
sigmas=sigmas, loops_allowed=loops_allowed)
|
|
950
|
+
BaseRefitting.__init__(self, edge_distrib=edge_distrib, max_rank=max_rank, loops_allowed=loops_allowed,
|
|
951
|
+
refit_threshold=refit_threshold)
|
|
952
|
+
self.lmbda = lmbda
|
|
953
|
+
self.alpha = alpha
|
|
954
|
+
|
|
955
|
+
def _compute_penalty_coef(self, idx: int, key: Tuple[int, int]) -> float:
|
|
956
|
+
"""
|
|
957
|
+
Compute the penalty coefficient for a specific parameter index.
|
|
958
|
+
|
|
959
|
+
Parameters
|
|
960
|
+
----------
|
|
961
|
+
idx : int
|
|
962
|
+
Index of the parameter matrix.
|
|
963
|
+
key : tuple of int
|
|
964
|
+
Network-layer key for selecting hyperparameters.
|
|
965
|
+
|
|
966
|
+
Returns
|
|
967
|
+
-------
|
|
968
|
+
float
|
|
969
|
+
Penalty coefficient applied to the soft-thresholding operator.
|
|
970
|
+
"""
|
|
971
|
+
hyperparam_dict = self._key_2_hyperparams_dict[key]
|
|
972
|
+
lmbda, alpha = hyperparam_dict["lmbda"], hyperparam_dict["alpha"]
|
|
973
|
+
return lmbda if idx == 0 else lmbda * alpha[idx - 1]
|
|
974
|
+
|
|
975
|
+
def _compute_projected_map(self, idx: int, lr, key: Tuple[int, int]):
|
|
976
|
+
"""
|
|
977
|
+
Generate a projected update function (soft-thresholding) for a parameter.
|
|
978
|
+
|
|
979
|
+
Parameters
|
|
980
|
+
----------
|
|
981
|
+
idx : int
|
|
982
|
+
Index of the parameter matrix.
|
|
983
|
+
lr : float
|
|
984
|
+
Learning rate.
|
|
985
|
+
key : tuple of int
|
|
986
|
+
Network-layer key.
|
|
987
|
+
|
|
988
|
+
Returns
|
|
989
|
+
-------
|
|
990
|
+
callable
|
|
991
|
+
A function that applies soft-thresholding with appropriate threshold.
|
|
992
|
+
"""
|
|
993
|
+
mask = self._param_participance_masks[idx]
|
|
994
|
+
penalty = self._compute_penalty_coef(idx=idx, key=key)
|
|
995
|
+
return partial(soft_thresholding_operator, threshold=penalty * lr / mask.sum(), max_rank=self.max_rank)
|
|
996
|
+
|
|
997
|
+
def _make_key_2_hyperparams_dict(self):
|
|
998
|
+
"""
|
|
999
|
+
Initialize the mapping from network-layer key to hyperparameters.
|
|
1000
|
+
|
|
1001
|
+
Sets default values for `lmbda` and `alpha` if not provided.
|
|
1002
|
+
"""
|
|
1003
|
+
lmbda = 2. * np.sqrt(self.n_layers_ * self.n_nodes_) if self.lmbda is None else self.lmbda
|
|
1004
|
+
if self.alpha is None:
|
|
1005
|
+
alpha = np.ones(self.n_layers_) / np.sqrt(self.n_layers_)
|
|
1006
|
+
else:
|
|
1007
|
+
alpha = if_scalar_or_given_length_array(self.alpha, length=self.n_layers_, name="alpha")
|
|
1008
|
+
self._key_2_hyperparams_dict = {(0, 0): {"lmbda": lmbda, "alpha": alpha}}
|
|
1009
|
+
|
|
1010
|
+
def fit(self, As: List[np.array],
|
|
1011
|
+
lr: float = 1.,
|
|
1012
|
+
max_iter: int = 100, tol=1e-5, patience=10,
|
|
1013
|
+
refit=True,
|
|
1014
|
+
verbose=True, verbose_interval=1,
|
|
1015
|
+
save_latent_history=False, save_nll_history=True):
|
|
1016
|
+
"""
|
|
1017
|
+
Fit the MultiNeSS model to a list of adjacency matrices.
|
|
1018
|
+
|
|
1019
|
+
Parameters
|
|
1020
|
+
----------
|
|
1021
|
+
As : list of np.ndarray
|
|
1022
|
+
Observed adjacency matrices.
|
|
1023
|
+
lr : float, default=1.
|
|
1024
|
+
Learning rate for optimization.
|
|
1025
|
+
max_iter : int, default=100
|
|
1026
|
+
Maximum number of iterations.
|
|
1027
|
+
tol : float, default=1e-5
|
|
1028
|
+
Convergence tolerance.
|
|
1029
|
+
patience : int, default=10
|
|
1030
|
+
Number of iterations for early stopping patience.
|
|
1031
|
+
refit : bool, default=True
|
|
1032
|
+
Whether to perform refitting after convergence.
|
|
1033
|
+
verbose : bool, default=True
|
|
1034
|
+
Whether to print progress messages.
|
|
1035
|
+
verbose_interval : int, default=1
|
|
1036
|
+
Interval between printing verbose messages.
|
|
1037
|
+
save_latent_history : bool, default=False
|
|
1038
|
+
Whether to save latent matrix history.
|
|
1039
|
+
save_nll_history : bool, default=True
|
|
1040
|
+
Whether to save negative log-likelihood history.
|
|
1041
|
+
|
|
1042
|
+
Returns
|
|
1043
|
+
-------
|
|
1044
|
+
self
|
|
1045
|
+
Fitted MultiNeSS object.
|
|
1046
|
+
"""
|
|
1047
|
+
As = self._validate_input(As)
|
|
1048
|
+
self._fit(As, lr=lr,
|
|
1049
|
+
max_iter=max_iter, tol=tol, patience=patience,
|
|
1050
|
+
refit=refit,
|
|
1051
|
+
verbose=verbose, verbose_interval=verbose_interval,
|
|
1052
|
+
save_latent_history=save_latent_history, save_nll_history=save_nll_history)
|
|
1053
|
+
return self
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
class MultiNeSSCV(MultiNeSS, BaseMultiNeSSRefinedCV):
|
|
1057
|
+
"""
|
|
1058
|
+
Cross-validated MultiNeSS model.
|
|
1059
|
+
|
|
1060
|
+
Extends `MultiNeSS` with hyperparameter selection via cross-validation.
|
|
1061
|
+
"""
|
|
1062
|
+
|
|
1063
|
+
def __init__(self, param_grid: Dict[str, List[float]] = None, cv_folds=3, test_prop=0.1,
|
|
1064
|
+
lmbda: float = None,
|
|
1065
|
+
alpha: Union[float, np.ndarray, list] = None,
|
|
1066
|
+
edge_distrib: str = "normal",
|
|
1067
|
+
max_rank: int = None, init_rank: int = None,
|
|
1068
|
+
sigmas: Union[float, List, np.ndarray] = None,
|
|
1069
|
+
loops_allowed: bool = True,
|
|
1070
|
+
refit_threshold: float = 1e-8,
|
|
1071
|
+
fix_layer_split: bool = False,
|
|
1072
|
+
use_1se_rule: bool = False,
|
|
1073
|
+
random_seed: int = None):
|
|
1074
|
+
"""
|
|
1075
|
+
Initialize a MultiNeSS model with cross-validation.
|
|
1076
|
+
|
|
1077
|
+
Parameters
|
|
1078
|
+
----------
|
|
1079
|
+
param_grid : dict of {str: list of float}, optional
|
|
1080
|
+
Grid of hyperparameters for CV.
|
|
1081
|
+
cv_folds : int, default=3
|
|
1082
|
+
Number of CV folds.
|
|
1083
|
+
test_prop : float, default=0.1
|
|
1084
|
+
Test set proportion for single split.
|
|
1085
|
+
lmbda : float, optional
|
|
1086
|
+
Global penalty factor.
|
|
1087
|
+
alpha : float, array-like, optional
|
|
1088
|
+
Layer-specific penalty factors.
|
|
1089
|
+
edge_distrib : str, default='normal'
|
|
1090
|
+
Edge distribution.
|
|
1091
|
+
max_rank : int, optional
|
|
1092
|
+
Maximum rank for projection.
|
|
1093
|
+
init_rank : int, optional
|
|
1094
|
+
Initial rank for initialization.
|
|
1095
|
+
sigmas : float or array-like, optional
|
|
1096
|
+
Edge standard deviations for 'normal' distribution.
|
|
1097
|
+
loops_allowed : bool, default=True
|
|
1098
|
+
Allow self-loops.
|
|
1099
|
+
refit_threshold : float, default=1e-8
|
|
1100
|
+
Threshold for refitting.
|
|
1101
|
+
fix_layer_split : bool, default=False
|
|
1102
|
+
Fix layer split across folds.
|
|
1103
|
+
use_1se_rule : bool, default=False
|
|
1104
|
+
Use 1-SE rule for selecting parsimonious model.
|
|
1105
|
+
random_seed : int, optional
|
|
1106
|
+
Random seed for reproducibility.
|
|
1107
|
+
"""
|
|
1108
|
+
BaseMultiNeSSRefinedCV.__init__(self, param_grid=param_grid, cv_folds=cv_folds, test_prop=test_prop,
|
|
1109
|
+
loops_allowed=loops_allowed,
|
|
1110
|
+
fix_layer_split=fix_layer_split,
|
|
1111
|
+
use_1se_rule=use_1se_rule,
|
|
1112
|
+
random_seed=random_seed)
|
|
1113
|
+
|
|
1114
|
+
MultiNeSS.__init__(self, lmbda=lmbda, alpha=alpha, edge_distrib=edge_distrib,
|
|
1115
|
+
max_rank=max_rank, init_rank=init_rank,
|
|
1116
|
+
sigmas=sigmas, loops_allowed=loops_allowed, refit_threshold=refit_threshold)
|
|
1117
|
+
|
|
1118
|
+
def _make_key_2_hyperparams_grid_dict(self) -> None:
|
|
1119
|
+
"""
|
|
1120
|
+
Create a default or user-specified hyperparameter grid for cross-validation.
|
|
1121
|
+
"""
|
|
1122
|
+
if self.param_grid is None:
|
|
1123
|
+
scale = np.sqrt(self.n_nodes_ * self.n_layers_)
|
|
1124
|
+
const_range = np.array([0.03, 0.1, 0.3, 1, 3, 10])
|
|
1125
|
+
param_grid = {"lmbda": scale * const_range} if self.param_grid is None else self.param_grid
|
|
1126
|
+
self._key_2_hyperparams_grid = {(0, 0): param_grid}
|
|
1127
|
+
else:
|
|
1128
|
+
self._key_2_hyperparams_grid = {(0, 0): self.param_grid}
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
class BaseOracleMultiNeSS(BaseMultiNeSSRefined):
|
|
1132
|
+
"""
|
|
1133
|
+
Oracle MultiNeSS with rank thresholding.
|
|
1134
|
+
|
|
1135
|
+
Parameters
|
|
1136
|
+
----------
|
|
1137
|
+
Inherits all parameters from BaseMultiNeSSRefined.
|
|
1138
|
+
"""
|
|
1139
|
+
|
|
1140
|
+
def _compute_threshold_rank(self, idx: int, key: Tuple[int, int]) -> int:
|
|
1141
|
+
"""
|
|
1142
|
+
Compute the threshold rank for hard-thresholding operator.
|
|
1143
|
+
|
|
1144
|
+
Parameters
|
|
1145
|
+
----------
|
|
1146
|
+
idx : int
|
|
1147
|
+
Parameter matrix index.
|
|
1148
|
+
key : tuple of int
|
|
1149
|
+
Network-layer key.
|
|
1150
|
+
|
|
1151
|
+
Returns
|
|
1152
|
+
-------
|
|
1153
|
+
int
|
|
1154
|
+
Threshold rank.
|
|
1155
|
+
"""
|
|
1156
|
+
pass
|
|
1157
|
+
|
|
1158
|
+
def _compute_projected_map(self, idx: int, lr, key: Tuple[int, int]):
|
|
1159
|
+
"""
|
|
1160
|
+
Generate a projected update function (hard-thresholding) for a parameter.
|
|
1161
|
+
|
|
1162
|
+
Parameters
|
|
1163
|
+
----------
|
|
1164
|
+
idx : int
|
|
1165
|
+
Index of the parameter matrix.
|
|
1166
|
+
lr : float
|
|
1167
|
+
Learning rate.
|
|
1168
|
+
key : tuple of int
|
|
1169
|
+
Network-layer key.
|
|
1170
|
+
|
|
1171
|
+
Returns
|
|
1172
|
+
-------
|
|
1173
|
+
callable
|
|
1174
|
+
Function applying hard-thresholding with computed max rank.
|
|
1175
|
+
"""
|
|
1176
|
+
rank = self._compute_threshold_rank(idx=idx, key=key)
|
|
1177
|
+
return partial(hard_thresholding_operator, max_rank=rank)
|
|
1178
|
+
|
|
1179
|
+
def _optimized_params_init(self, As, key: Tuple[int, int]) -> None:
|
|
1180
|
+
"""
|
|
1181
|
+
Initialize parameter matrices using hard-thresholding with threshold ranks.
|
|
1182
|
+
|
|
1183
|
+
Parameters
|
|
1184
|
+
----------
|
|
1185
|
+
As : list of np.ndarray
|
|
1186
|
+
Observed adjacency matrices.
|
|
1187
|
+
key : tuple of int
|
|
1188
|
+
Network-layer key.
|
|
1189
|
+
"""
|
|
1190
|
+
param_indices = self._key_2_param_indices[key]
|
|
1191
|
+
param_inits = []
|
|
1192
|
+
for idx in param_indices:
|
|
1193
|
+
mask = self._param_participance_masks[idx]
|
|
1194
|
+
max_rank = self._compute_threshold_rank(idx=idx, key=key)
|
|
1195
|
+
param_init = hard_thresholding_operator(np.nansum(As[mask], axis=0) / mask.sum(), max_rank=max_rank)
|
|
1196
|
+
param_inits.append(param_init)
|
|
1197
|
+
self._set_matrices(param_inits, indices=param_indices)
|
|
1198
|
+
|
|
1199
|
+
def fit(self, As: List[np.array],
|
|
1200
|
+
lr: float = 1.,
|
|
1201
|
+
max_iter: int = 100, tol=1e-5, patience=10,
|
|
1202
|
+
verbose=True, verbose_interval=1,
|
|
1203
|
+
save_latent_history=False, save_nll_history=True):
|
|
1204
|
+
"""
|
|
1205
|
+
Fit the oracle MultiNeSS model.
|
|
1206
|
+
|
|
1207
|
+
Parameters
|
|
1208
|
+
----------
|
|
1209
|
+
As : list of np.ndarray
|
|
1210
|
+
Observed adjacency matrices.
|
|
1211
|
+
lr : float, default=1.
|
|
1212
|
+
Learning rate for optimization.
|
|
1213
|
+
max_iter : int, default=100
|
|
1214
|
+
Maximum number of iterations.
|
|
1215
|
+
tol : float, default=1e-5
|
|
1216
|
+
Convergence tolerance.
|
|
1217
|
+
patience : int, default=10
|
|
1218
|
+
Patience for early stopping.
|
|
1219
|
+
verbose : bool, default=True
|
|
1220
|
+
Print progress messages.
|
|
1221
|
+
verbose_interval : int, default=1
|
|
1222
|
+
Verbose print interval.
|
|
1223
|
+
save_latent_history : bool, default=False
|
|
1224
|
+
Whether to store latent matrices at each iteration.
|
|
1225
|
+
save_nll_history : bool, default=True
|
|
1226
|
+
Whether to store NLL at each iteration.
|
|
1227
|
+
|
|
1228
|
+
Returns
|
|
1229
|
+
-------
|
|
1230
|
+
self
|
|
1231
|
+
Fitted oracle MultiNeSS model.
|
|
1232
|
+
"""
|
|
1233
|
+
As = self._validate_input(As)
|
|
1234
|
+
self._fit(As, lr=lr,
|
|
1235
|
+
max_iter=max_iter, tol=tol, patience=patience,
|
|
1236
|
+
verbose=verbose, verbose_interval=verbose_interval,
|
|
1237
|
+
refit=False,
|
|
1238
|
+
save_latent_history=save_latent_history, save_nll_history=save_nll_history)
|
|
1239
|
+
return self
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
class OracleMultiNeSS(BaseOracleMultiNeSS):
|
|
1243
|
+
"""
|
|
1244
|
+
Oracle MultiNeSS model with fixed shared and individual ranks.
|
|
1245
|
+
|
|
1246
|
+
Inherits from `BaseOracleMultiNeSS` and sets the threshold ranks based on
|
|
1247
|
+
user-specified shared and individual layer dimensions.
|
|
1248
|
+
|
|
1249
|
+
Parameters
|
|
1250
|
+
----------
|
|
1251
|
+
d_shared : int
|
|
1252
|
+
Rank of the shared component across all layers.
|
|
1253
|
+
d_individs : int or list of int
|
|
1254
|
+
Ranks of the individual components for each layer.
|
|
1255
|
+
edge_distrib : str, default='normal'
|
|
1256
|
+
Edge distribution ('normal' or 'bernoulli').
|
|
1257
|
+
loops_allowed : bool, default=True
|
|
1258
|
+
Whether self-loops are allowed in the network.
|
|
1259
|
+
"""
|
|
1260
|
+
|
|
1261
|
+
def __init__(self, d_shared: int, d_individs: Union[int, List[int]],
|
|
1262
|
+
edge_distrib: str = "normal", loops_allowed: bool = True):
|
|
1263
|
+
"""
|
|
1264
|
+
Initialize the OracleMultiNeSS model.
|
|
1265
|
+
|
|
1266
|
+
Parameters
|
|
1267
|
+
----------
|
|
1268
|
+
d_shared : int
|
|
1269
|
+
Rank of the shared component.
|
|
1270
|
+
d_individs : int or list of int
|
|
1271
|
+
Rank(s) of individual components for each layer.
|
|
1272
|
+
edge_distrib : str, default='normal'
|
|
1273
|
+
Edge distribution.
|
|
1274
|
+
loops_allowed : bool, default=True
|
|
1275
|
+
Whether self-loops are allowed.
|
|
1276
|
+
"""
|
|
1277
|
+
BaseMultiNeSSRefined.__init__(self, edge_distrib=edge_distrib, loops_allowed=loops_allowed)
|
|
1278
|
+
self.d_shared = d_shared
|
|
1279
|
+
self.d_individs = d_individs
|
|
1280
|
+
|
|
1281
|
+
def _make_key_2_hyperparams_dict(self):
|
|
1282
|
+
"""
|
|
1283
|
+
Initialize the mapping from network-layer key to hyperparameters.
|
|
1284
|
+
"""
|
|
1285
|
+
d_shared = if_scalar_or_given_length_array(self.d_shared, length=1, name="d_shared")
|
|
1286
|
+
d_individs = if_scalar_or_given_length_array(self.d_individs, length=self.n_layers_, name="d_individs")
|
|
1287
|
+
self._key_2_hyperparams_dict = {(0, 0): {"d_shared": d_shared, "d_individs": d_individs}}
|
|
1288
|
+
|
|
1289
|
+
def _compute_threshold_rank(self, idx: int, key: Tuple[int, int]) -> int:
|
|
1290
|
+
"""
|
|
1291
|
+
Compute the threshold rank for the hard-thresholding operator.
|
|
1292
|
+
|
|
1293
|
+
Parameters
|
|
1294
|
+
----------
|
|
1295
|
+
idx : int
|
|
1296
|
+
Index of the parameter matrix (0 for shared component, >0 for individual layers).
|
|
1297
|
+
key : tuple of int
|
|
1298
|
+
Network-layer key.
|
|
1299
|
+
|
|
1300
|
+
Returns
|
|
1301
|
+
-------
|
|
1302
|
+
int
|
|
1303
|
+
Rank threshold for hard-thresholding.
|
|
1304
|
+
"""
|
|
1305
|
+
hyperparam_dict = self._key_2_hyperparams_dict[key]
|
|
1306
|
+
d_shared, d_individs = hyperparam_dict["d_shared"], hyperparam_dict["d_individs"]
|
|
1307
|
+
return d_shared if idx == 0 else d_individs[idx - 1]
|