dimc-toolkit 1.0.0__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.
- dimc/__init__.py +13 -0
- dimc/algorithms/__init__.py +23 -0
- dimc/algorithms/update_H.py +50 -0
- dimc/algorithms/update_W.py +42 -0
- dimc/algorithms/update_graph.py +87 -0
- dimc/algorithms/update_weights.py +39 -0
- dimc/config.py +84 -0
- dimc/datasets/__init__.py +5 -0
- dimc/datasets/load_dataset.py +13 -0
- dimc/fit.py +682 -0
- dimc/graph.py +55 -0
- dimc/initialize.py +156 -0
- dimc/losses.py +18 -0
- dimc/metrics.py +81 -0
- dimc/preprocessing.py +131 -0
- dimc/updates.py +20 -0
- dimc/utils.py +32 -0
- dimc_toolkit-1.0.0.dist-info/METADATA +31 -0
- dimc_toolkit-1.0.0.dist-info/RECORD +22 -0
- dimc_toolkit-1.0.0.dist-info/WHEEL +5 -0
- dimc_toolkit-1.0.0.dist-info/licenses/LICENSE +0 -0
- dimc_toolkit-1.0.0.dist-info/top_level.txt +1 -0
dimc/fit.py
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Alternating optimization loop, alpha-sensitivity sweep, and full pipeline.
|
|
3
|
+
|
|
4
|
+
Corresponds to notebook Sections 8, 9b, and 10:
|
|
5
|
+
8. Algorithm 1, lines 4-9: the alternating optimization loop
|
|
6
|
+
9b. Parameter sensitivity sweep for alpha(1)/alpha(2) (Section 4.4, Fig. 4)
|
|
7
|
+
10. Full pipeline = Algorithm 1, driven by one plain settings dict
|
|
8
|
+
|
|
9
|
+
The public entry point is ``fit`` (aliased to ``run_dimc`` for backward
|
|
10
|
+
compatibility with the notebook's naming), matching the requested usage::
|
|
11
|
+
|
|
12
|
+
from dimc import fit
|
|
13
|
+
result = fit(X_views, labels, learning_rate=1e-5, alpha_1=60, alpha_2=60)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any, Dict, List, Optional, Sequence
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import torch
|
|
21
|
+
|
|
22
|
+
from dimc.algorithms.update_graph import update_PC, graph_regularization_loss
|
|
23
|
+
from dimc.algorithms.update_weights import update_PI
|
|
24
|
+
from dimc.algorithms.update_W import update_U
|
|
25
|
+
from dimc.algorithms.update_H import finetune_view_network
|
|
26
|
+
from dimc.config import DEFAULT_ALPHA_CANDIDATES, DEFAULT_SETTINGS, SWEEP_MAX_EPOCHS
|
|
27
|
+
from dimc.graph import build_view_graph
|
|
28
|
+
from dimc.initialize import (
|
|
29
|
+
build_view_decoder,
|
|
30
|
+
build_view_network,
|
|
31
|
+
initialize_nmf_factor,
|
|
32
|
+
pretrain_view_network,
|
|
33
|
+
)
|
|
34
|
+
from dimc.metrics import cluster_representation, compute_ACC, compute_NMI
|
|
35
|
+
from dimc.preprocessing import (
|
|
36
|
+
load_views,
|
|
37
|
+
make_missing_masks,
|
|
38
|
+
split_by_availability,
|
|
39
|
+
standardize_views,
|
|
40
|
+
)
|
|
41
|
+
from dimc.utils import set_global_seed
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ============================================================
|
|
45
|
+
# 8. Algorithm 1, lines 4-9: the alternating optimization loop
|
|
46
|
+
# ============================================================
|
|
47
|
+
|
|
48
|
+
def _forward_all_views(
|
|
49
|
+
encoders: Sequence[Any],
|
|
50
|
+
X_views_t: Sequence[torch.Tensor],
|
|
51
|
+
) -> List[np.ndarray]:
|
|
52
|
+
"""
|
|
53
|
+
Run every view's current encoder to obtain H(v) for all views
|
|
54
|
+
(Algorithm 1, line 5: forward propagation).
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
encoders : Sequence[torch.nn.Sequential]
|
|
59
|
+
Per-view encoder networks.
|
|
60
|
+
X_views_t : Sequence[torch.Tensor]
|
|
61
|
+
Per-view input tensors, each shape ``(n_samples, input_dim_v)``.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
List[numpy.ndarray]
|
|
66
|
+
Per-view latent representations ``H(v)``, each shape
|
|
67
|
+
``(n_samples, latent_dim)``.
|
|
68
|
+
"""
|
|
69
|
+
return [enc(X).detach().numpy() for enc, X in zip(encoders, X_views_t)]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _update_codes(
|
|
73
|
+
U_list: List[np.ndarray],
|
|
74
|
+
PC: np.ndarray,
|
|
75
|
+
PI_list: List[np.ndarray],
|
|
76
|
+
H_complete: List[np.ndarray],
|
|
77
|
+
H_private: List[np.ndarray],
|
|
78
|
+
W_list: List[np.ndarray],
|
|
79
|
+
D_list: List[np.ndarray],
|
|
80
|
+
alpha_list: List[float],
|
|
81
|
+
) -> tuple:
|
|
82
|
+
"""
|
|
83
|
+
Algorithm 1, line 6: update PC (Eq. 13) and each P(v)_I (Eq. 14).
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
U_list : List[numpy.ndarray]
|
|
88
|
+
Per-view basis matrices.
|
|
89
|
+
PC : numpy.ndarray
|
|
90
|
+
Current shared code matrix.
|
|
91
|
+
PI_list : List[numpy.ndarray]
|
|
92
|
+
Per-view private code matrices.
|
|
93
|
+
H_complete : List[numpy.ndarray]
|
|
94
|
+
Per-view DNN latent representations restricted to complete samples.
|
|
95
|
+
H_private : List[numpy.ndarray]
|
|
96
|
+
Per-view DNN latent representations restricted to private samples.
|
|
97
|
+
W_list : List[numpy.ndarray]
|
|
98
|
+
Per-view affinity matrices.
|
|
99
|
+
D_list : List[numpy.ndarray]
|
|
100
|
+
Per-view degree matrices.
|
|
101
|
+
alpha_list : List[float]
|
|
102
|
+
Per-view graph-regularization strengths.
|
|
103
|
+
|
|
104
|
+
Returns
|
|
105
|
+
-------
|
|
106
|
+
PC : numpy.ndarray
|
|
107
|
+
Updated shared code matrix.
|
|
108
|
+
PI_list : List[numpy.ndarray]
|
|
109
|
+
Updated per-view private code matrices.
|
|
110
|
+
"""
|
|
111
|
+
PC = update_PC(PC, U_list, H_complete, W_list, D_list, alpha_list)
|
|
112
|
+
PI_list = [update_PI(PI, U, H) for PI, U, H in zip(PI_list, U_list, H_private)]
|
|
113
|
+
return PC, PI_list
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _update_bases(
|
|
117
|
+
U_list: List[np.ndarray],
|
|
118
|
+
H_full: List[np.ndarray],
|
|
119
|
+
obs_idx: List[np.ndarray],
|
|
120
|
+
PC: np.ndarray,
|
|
121
|
+
PI_list: List[np.ndarray],
|
|
122
|
+
) -> List[np.ndarray]:
|
|
123
|
+
"""
|
|
124
|
+
Algorithm 1, line 7: update each U(v) (Eq. 16), using the FULL H(v)
|
|
125
|
+
and P(v) = [PC, P(v)_I].
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
U_list : List[numpy.ndarray]
|
|
130
|
+
Per-view basis matrices.
|
|
131
|
+
H_full : List[numpy.ndarray]
|
|
132
|
+
Per-view full DNN latent representations, each shape
|
|
133
|
+
``(n_samples, latent_dim)``.
|
|
134
|
+
obs_idx : List[numpy.ndarray]
|
|
135
|
+
Per-view indices of samples that view observes.
|
|
136
|
+
PC : numpy.ndarray
|
|
137
|
+
Current shared code matrix.
|
|
138
|
+
PI_list : List[numpy.ndarray]
|
|
139
|
+
Per-view private code matrices.
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
List[numpy.ndarray]
|
|
144
|
+
Updated per-view basis matrices.
|
|
145
|
+
"""
|
|
146
|
+
return [
|
|
147
|
+
update_U(U, H[idx].T, np.hstack([PC, PI]))
|
|
148
|
+
for U, H, idx, PI in zip(U_list, H_full, obs_idx, PI_list)
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _finetune_all_views(
|
|
153
|
+
encoders: Sequence[Any],
|
|
154
|
+
X_views_t: Sequence[torch.Tensor],
|
|
155
|
+
obs_idx: List[np.ndarray],
|
|
156
|
+
U_list: List[np.ndarray],
|
|
157
|
+
PC: np.ndarray,
|
|
158
|
+
PI_list: List[np.ndarray],
|
|
159
|
+
learning_rate: float,
|
|
160
|
+
) -> List[float]:
|
|
161
|
+
"""
|
|
162
|
+
Algorithm 1, line 8: fine-tune each view-specific DNN (Eqs. 17-23).
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
encoders : Sequence[torch.nn.Sequential]
|
|
167
|
+
Per-view encoder networks, modified in place.
|
|
168
|
+
X_views_t : Sequence[torch.Tensor]
|
|
169
|
+
Per-view input tensors.
|
|
170
|
+
obs_idx : List[numpy.ndarray]
|
|
171
|
+
Per-view indices of samples that view observes.
|
|
172
|
+
U_list : List[numpy.ndarray]
|
|
173
|
+
Per-view basis matrices.
|
|
174
|
+
PC : numpy.ndarray
|
|
175
|
+
Current shared code matrix.
|
|
176
|
+
PI_list : List[numpy.ndarray]
|
|
177
|
+
Per-view private code matrices.
|
|
178
|
+
learning_rate : float
|
|
179
|
+
Plain gradient-descent step size for the fine-tune step.
|
|
180
|
+
|
|
181
|
+
Returns
|
|
182
|
+
-------
|
|
183
|
+
List[float]
|
|
184
|
+
Per-view MSE reconstruction loss values.
|
|
185
|
+
"""
|
|
186
|
+
losses = []
|
|
187
|
+
for v in range(2):
|
|
188
|
+
P_full = np.hstack([PC, PI_list[v]])
|
|
189
|
+
target = torch.tensor((U_list[v] @ P_full).T, dtype=torch.float32)
|
|
190
|
+
losses.append(
|
|
191
|
+
finetune_view_network(encoders[v], X_views_t[v][obs_idx[v]], target, learning_rate)
|
|
192
|
+
)
|
|
193
|
+
return losses
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def train_dimc(
|
|
197
|
+
X_views_t: Sequence[torch.Tensor],
|
|
198
|
+
encoders: Sequence[Any],
|
|
199
|
+
U_list: List[np.ndarray],
|
|
200
|
+
PC: np.ndarray,
|
|
201
|
+
PI_list: List[np.ndarray],
|
|
202
|
+
split: Dict[str, np.ndarray],
|
|
203
|
+
graphs: Sequence[Dict[str, np.ndarray]],
|
|
204
|
+
alpha_list: List[float],
|
|
205
|
+
learning_rate: float = 1e-2,
|
|
206
|
+
n_epochs: int = 300,
|
|
207
|
+
log_every: int = 50,
|
|
208
|
+
) -> tuple:
|
|
209
|
+
"""
|
|
210
|
+
Run the Algorithm 1, lines 4-9 alternating optimization loop.
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
X_views_t : Sequence[torch.Tensor]
|
|
215
|
+
Per-view input tensors, each shape ``(n_samples, input_dim_v)``.
|
|
216
|
+
encoders : Sequence[torch.nn.Sequential]
|
|
217
|
+
Per-view encoder networks, modified in place during training.
|
|
218
|
+
U_list : List[numpy.ndarray]
|
|
219
|
+
Initial per-view basis matrices.
|
|
220
|
+
PC : numpy.ndarray
|
|
221
|
+
Initial shared code matrix.
|
|
222
|
+
PI_list : List[numpy.ndarray]
|
|
223
|
+
Initial per-view private code matrices.
|
|
224
|
+
split : Dict[str, numpy.ndarray]
|
|
225
|
+
Output of ``dimc.preprocessing.split_by_availability``.
|
|
226
|
+
graphs : Sequence[Dict[str, numpy.ndarray]]
|
|
227
|
+
Per-view graph dictionaries from ``dimc.graph.build_view_graph``,
|
|
228
|
+
each containing ``"W"``, ``"D"``, and ``"L"``.
|
|
229
|
+
alpha_list : List[float]
|
|
230
|
+
Per-view graph-regularization strengths.
|
|
231
|
+
learning_rate : float, default=1e-2
|
|
232
|
+
Learning rate used for the DNN fine-tuning step.
|
|
233
|
+
n_epochs : int, default=300
|
|
234
|
+
Number of alternating-optimization iterations.
|
|
235
|
+
log_every : int, default=50
|
|
236
|
+
Print reconstruction/graph loss every this many epochs.
|
|
237
|
+
|
|
238
|
+
Returns
|
|
239
|
+
-------
|
|
240
|
+
U_list : List[numpy.ndarray]
|
|
241
|
+
Final per-view basis matrices.
|
|
242
|
+
PC : numpy.ndarray
|
|
243
|
+
Final shared code matrix.
|
|
244
|
+
PI_list : List[numpy.ndarray]
|
|
245
|
+
Final per-view private code matrices.
|
|
246
|
+
"""
|
|
247
|
+
complete_idx = split["complete_idx"]
|
|
248
|
+
private_idx = [split["view0_only_idx"], split["view1_only_idx"]]
|
|
249
|
+
obs_idx = [split["view0_obs_idx"], split["view1_obs_idx"]]
|
|
250
|
+
|
|
251
|
+
W_list = [g["W"] for g in graphs]
|
|
252
|
+
D_list = [g["D"] for g in graphs]
|
|
253
|
+
L_list = [g["L"] for g in graphs]
|
|
254
|
+
|
|
255
|
+
for epoch in range(n_epochs):
|
|
256
|
+
# line 5: forward propagation -> H(v) for all views
|
|
257
|
+
H_full = _forward_all_views(encoders, X_views_t)
|
|
258
|
+
H_complete = [H[complete_idx].T for H in H_full]
|
|
259
|
+
H_private = [H[idx].T for H, idx in zip(H_full, private_idx)]
|
|
260
|
+
|
|
261
|
+
# line 6: update PC (Eq. 13) and each P(v)_I (Eq. 14)
|
|
262
|
+
PC, PI_list = _update_codes(
|
|
263
|
+
U_list, PC, PI_list, H_complete, H_private, W_list, D_list, alpha_list
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# line 7: update each U(v) (Eq. 16), using the FULL H(v) and P(v) = [PC, P(v)_I]
|
|
267
|
+
U_list = _update_bases(U_list, H_full, obs_idx, PC, PI_list)
|
|
268
|
+
|
|
269
|
+
# line 8: fine-tune each view-specific DNN (Eqs. 17-23)
|
|
270
|
+
losses = _finetune_all_views(encoders, X_views_t, obs_idx, U_list, PC, PI_list, learning_rate)
|
|
271
|
+
|
|
272
|
+
if epoch % log_every == 0:
|
|
273
|
+
recon_loss = sum(losses)
|
|
274
|
+
graph_loss = sum(
|
|
275
|
+
graph_regularization_loss(PC, L, alpha)
|
|
276
|
+
for L, alpha in zip(L_list, alpha_list)
|
|
277
|
+
)
|
|
278
|
+
print(f"Epoch {epoch:3d} | Recon: {recon_loss:.4f} | Graph: {graph_loss:.4f}")
|
|
279
|
+
|
|
280
|
+
return U_list, PC, PI_list
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
# ============================================================
|
|
284
|
+
# 10. Full pipeline = Algorithm 1, driven by one plain settings dict
|
|
285
|
+
# ============================================================
|
|
286
|
+
|
|
287
|
+
def _prepare_data(
|
|
288
|
+
settings: Dict[str, Any],
|
|
289
|
+
X_views: Optional[Sequence[np.ndarray]],
|
|
290
|
+
labels: Optional[np.ndarray],
|
|
291
|
+
) -> tuple:
|
|
292
|
+
"""
|
|
293
|
+
Load (if needed) and standardize the input views, and derive labels
|
|
294
|
+
if not provided (Section 4.1).
|
|
295
|
+
|
|
296
|
+
Parameters
|
|
297
|
+
----------
|
|
298
|
+
settings : Dict[str, Any]
|
|
299
|
+
Full settings dictionary.
|
|
300
|
+
X_views : Optional[Sequence[numpy.ndarray]]
|
|
301
|
+
Pre-loaded views, or ``None`` to load from ``settings["fou_path"]``
|
|
302
|
+
/ ``settings["pix_path"]``.
|
|
303
|
+
labels : Optional[numpy.ndarray]
|
|
304
|
+
Ground-truth labels, or ``None`` to derive from
|
|
305
|
+
``settings["n_classes"]`` / ``settings["samples_per_class"]``.
|
|
306
|
+
|
|
307
|
+
Returns
|
|
308
|
+
-------
|
|
309
|
+
X_views : List[numpy.ndarray]
|
|
310
|
+
Standardized views.
|
|
311
|
+
labels : numpy.ndarray
|
|
312
|
+
Ground-truth labels.
|
|
313
|
+
n_samples : int
|
|
314
|
+
Number of samples (rows) in view 0.
|
|
315
|
+
"""
|
|
316
|
+
if X_views is None:
|
|
317
|
+
X_views = load_views([settings["fou_path"], settings["pix_path"]])
|
|
318
|
+
X_views = standardize_views(X_views)
|
|
319
|
+
n_samples = X_views[0].shape[0]
|
|
320
|
+
|
|
321
|
+
if labels is None:
|
|
322
|
+
labels = np.repeat(np.arange(settings["n_classes"]), settings["samples_per_class"])
|
|
323
|
+
|
|
324
|
+
return X_views, labels, n_samples
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _build_and_pretrain_networks(
|
|
328
|
+
settings: Dict[str, Any],
|
|
329
|
+
X_views: Sequence[np.ndarray],
|
|
330
|
+
X_views_t: Sequence[torch.Tensor],
|
|
331
|
+
) -> tuple:
|
|
332
|
+
"""
|
|
333
|
+
Algorithm 1, line 1: build and pretrain each view-specific DNN
|
|
334
|
+
(encoder + decoder) by auto-encoding.
|
|
335
|
+
|
|
336
|
+
Parameters
|
|
337
|
+
----------
|
|
338
|
+
settings : Dict[str, Any]
|
|
339
|
+
Full settings dictionary.
|
|
340
|
+
X_views : Sequence[numpy.ndarray]
|
|
341
|
+
Standardized views (used only for their feature dimensionality).
|
|
342
|
+
X_views_t : Sequence[torch.Tensor]
|
|
343
|
+
Torch tensors of the standardized views.
|
|
344
|
+
|
|
345
|
+
Returns
|
|
346
|
+
-------
|
|
347
|
+
encoders : List[torch.nn.Sequential]
|
|
348
|
+
Pretrained per-view encoders.
|
|
349
|
+
decoders : List[torch.nn.Sequential]
|
|
350
|
+
Pretrained per-view decoders.
|
|
351
|
+
"""
|
|
352
|
+
encoders = [
|
|
353
|
+
build_view_network(X.shape[1], latent_dim=settings["latent_dim"]) for X in X_views
|
|
354
|
+
]
|
|
355
|
+
decoders = [
|
|
356
|
+
build_view_decoder(X.shape[1], latent_dim=settings["latent_dim"]) for X in X_views
|
|
357
|
+
]
|
|
358
|
+
|
|
359
|
+
for enc, dec, X in zip(encoders, decoders, X_views_t):
|
|
360
|
+
pretrain_view_network(
|
|
361
|
+
enc, dec, X,
|
|
362
|
+
epochs=settings["pretrain_epochs"],
|
|
363
|
+
learning_rate=settings["pretrain_lr"],
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return encoders, decoders
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _initialize_factors(
|
|
370
|
+
settings: Dict[str, Any],
|
|
371
|
+
split: Dict[str, np.ndarray],
|
|
372
|
+
rng: np.random.RandomState,
|
|
373
|
+
) -> tuple:
|
|
374
|
+
"""
|
|
375
|
+
Algorithm 1, lines 2-3: initialize U(v), PC (Eq. 7), and P(v)_I randomly.
|
|
376
|
+
|
|
377
|
+
Parameters
|
|
378
|
+
----------
|
|
379
|
+
settings : Dict[str, Any]
|
|
380
|
+
Full settings dictionary.
|
|
381
|
+
split : Dict[str, numpy.ndarray]
|
|
382
|
+
Output of ``dimc.preprocessing.split_by_availability``.
|
|
383
|
+
rng : numpy.random.RandomState
|
|
384
|
+
Random state used for all factor initializations.
|
|
385
|
+
|
|
386
|
+
Returns
|
|
387
|
+
-------
|
|
388
|
+
U_list : List[numpy.ndarray]
|
|
389
|
+
Initial per-view basis matrices.
|
|
390
|
+
PC : numpy.ndarray
|
|
391
|
+
Initial shared code matrix.
|
|
392
|
+
PI_list : List[numpy.ndarray]
|
|
393
|
+
Initial per-view private code matrices.
|
|
394
|
+
"""
|
|
395
|
+
complete_idx = split["complete_idx"]
|
|
396
|
+
|
|
397
|
+
U_list = [
|
|
398
|
+
initialize_nmf_factor((settings["latent_dim"], settings["latent_k"]), rng)
|
|
399
|
+
for _ in range(2)
|
|
400
|
+
]
|
|
401
|
+
PC = initialize_nmf_factor((settings["latent_k"], len(complete_idx)), rng)
|
|
402
|
+
PI_list = [
|
|
403
|
+
initialize_nmf_factor((settings["latent_k"], len(idx)), rng)
|
|
404
|
+
for idx in [split["view0_only_idx"], split["view1_only_idx"]]
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
return U_list, PC, PI_list
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _nmf_warmup(
|
|
411
|
+
settings: Dict[str, Any],
|
|
412
|
+
encoders: Sequence[Any],
|
|
413
|
+
X_views_t: Sequence[torch.Tensor],
|
|
414
|
+
split: Dict[str, np.ndarray],
|
|
415
|
+
graphs: Sequence[Dict[str, np.ndarray]],
|
|
416
|
+
U_list: List[np.ndarray],
|
|
417
|
+
PC: np.ndarray,
|
|
418
|
+
alpha_list: List[float],
|
|
419
|
+
) -> tuple:
|
|
420
|
+
"""
|
|
421
|
+
NMF-only warm-up on the complete samples to minimize Eq. (7) before
|
|
422
|
+
joint training.
|
|
423
|
+
|
|
424
|
+
Parameters
|
|
425
|
+
----------
|
|
426
|
+
settings : Dict[str, Any]
|
|
427
|
+
Full settings dictionary.
|
|
428
|
+
encoders : Sequence[torch.nn.Sequential]
|
|
429
|
+
Pretrained per-view encoders.
|
|
430
|
+
X_views_t : Sequence[torch.Tensor]
|
|
431
|
+
Torch tensors of the standardized views.
|
|
432
|
+
split : Dict[str, numpy.ndarray]
|
|
433
|
+
Output of ``dimc.preprocessing.split_by_availability``.
|
|
434
|
+
graphs : Sequence[Dict[str, numpy.ndarray]]
|
|
435
|
+
Per-view graph dictionaries.
|
|
436
|
+
U_list : List[numpy.ndarray]
|
|
437
|
+
Initial per-view basis matrices.
|
|
438
|
+
PC : numpy.ndarray
|
|
439
|
+
Initial shared code matrix.
|
|
440
|
+
alpha_list : List[float]
|
|
441
|
+
Per-view graph-regularization strengths.
|
|
442
|
+
|
|
443
|
+
Returns
|
|
444
|
+
-------
|
|
445
|
+
U_list : List[numpy.ndarray]
|
|
446
|
+
Warmed-up per-view basis matrices.
|
|
447
|
+
PC : numpy.ndarray
|
|
448
|
+
Warmed-up shared code matrix.
|
|
449
|
+
"""
|
|
450
|
+
complete_idx = split["complete_idx"]
|
|
451
|
+
|
|
452
|
+
H0 = encoders[0](X_views_t[0]).detach().numpy()
|
|
453
|
+
H1 = encoders[1](X_views_t[1]).detach().numpy()
|
|
454
|
+
H_complete = [H0[complete_idx].T, H1[complete_idx].T]
|
|
455
|
+
W_list = [g["W"] for g in graphs]
|
|
456
|
+
D_list = [g["D"] for g in graphs]
|
|
457
|
+
|
|
458
|
+
for _ in range(settings["nmf_pretrain_iters"]):
|
|
459
|
+
PC = update_PC(PC, U_list, H_complete, W_list, D_list, alpha_list)
|
|
460
|
+
U_list = [update_U(U, H, PC) for U, H in zip(U_list, H_complete)]
|
|
461
|
+
|
|
462
|
+
return U_list, PC
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _assemble_shared_representation(
|
|
466
|
+
settings: Dict[str, Any],
|
|
467
|
+
split: Dict[str, np.ndarray],
|
|
468
|
+
n_samples: int,
|
|
469
|
+
PC: np.ndarray,
|
|
470
|
+
PI_list: List[np.ndarray],
|
|
471
|
+
) -> np.ndarray:
|
|
472
|
+
"""
|
|
473
|
+
Assemble the full shared representation P by scattering PC and each
|
|
474
|
+
P(v)_I back into their original sample positions (Section 3, end).
|
|
475
|
+
|
|
476
|
+
Parameters
|
|
477
|
+
----------
|
|
478
|
+
settings : Dict[str, Any]
|
|
479
|
+
Full settings dictionary.
|
|
480
|
+
split : Dict[str, numpy.ndarray]
|
|
481
|
+
Output of ``dimc.preprocessing.split_by_availability``.
|
|
482
|
+
n_samples : int
|
|
483
|
+
Total number of samples.
|
|
484
|
+
PC : numpy.ndarray
|
|
485
|
+
Final shared code matrix, restricted to complete samples.
|
|
486
|
+
PI_list : List[numpy.ndarray]
|
|
487
|
+
Final per-view private code matrices.
|
|
488
|
+
|
|
489
|
+
Returns
|
|
490
|
+
-------
|
|
491
|
+
numpy.ndarray
|
|
492
|
+
Full shared representation, shape ``(latent_k, n_samples)``.
|
|
493
|
+
"""
|
|
494
|
+
P_shared = np.zeros((settings["latent_k"], n_samples), dtype=np.float32)
|
|
495
|
+
P_shared[:, split["complete_idx"]] = PC
|
|
496
|
+
P_shared[:, split["view0_only_idx"]] = PI_list[0]
|
|
497
|
+
P_shared[:, split["view1_only_idx"]] = PI_list[1]
|
|
498
|
+
return P_shared
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def run_dimc(
|
|
502
|
+
settings: Dict[str, Any],
|
|
503
|
+
X_views: Optional[Sequence[np.ndarray]] = None,
|
|
504
|
+
labels: Optional[np.ndarray] = None,
|
|
505
|
+
) -> Dict[str, Any]:
|
|
506
|
+
"""
|
|
507
|
+
Run the full DIMC pipeline (Algorithm 1) end to end, driven by one
|
|
508
|
+
plain settings dictionary.
|
|
509
|
+
|
|
510
|
+
Parameters
|
|
511
|
+
----------
|
|
512
|
+
settings : Dict[str, Any]
|
|
513
|
+
Full settings dictionary; see ``dimc.config.DEFAULT_SETTINGS``
|
|
514
|
+
for the expected keys and their meanings.
|
|
515
|
+
X_views : Optional[Sequence[numpy.ndarray]], default=None
|
|
516
|
+
Pre-loaded views. If ``None``, views are loaded from
|
|
517
|
+
``settings["fou_path"]`` / ``settings["pix_path"]``.
|
|
518
|
+
labels : Optional[numpy.ndarray], default=None
|
|
519
|
+
Ground-truth labels. If ``None``, labels are derived from
|
|
520
|
+
``settings["n_classes"]`` / ``settings["samples_per_class"]``.
|
|
521
|
+
|
|
522
|
+
Returns
|
|
523
|
+
-------
|
|
524
|
+
Dict[str, Any]
|
|
525
|
+
Dictionary with keys ``"ACC"``, ``"NMI"``, ``"encoders"``,
|
|
526
|
+
``"decoders"``, ``"U_list"``, ``"PC"``, ``"PI_list"``,
|
|
527
|
+
``"P_shared"``, and ``"cluster_labels"``.
|
|
528
|
+
"""
|
|
529
|
+
t0 = time.time()
|
|
530
|
+
|
|
531
|
+
seed = settings["seed"]
|
|
532
|
+
rng = set_global_seed(seed)
|
|
533
|
+
|
|
534
|
+
# ---- data (Section 4.1) ----
|
|
535
|
+
X_views, labels, n_samples = _prepare_data(settings, X_views, labels)
|
|
536
|
+
|
|
537
|
+
# ---- incomplete-view split: D = {X_C, X_I} ----
|
|
538
|
+
mask_0, mask_1 = make_missing_masks(n_samples, settings["missing_ratio"], settings["mask_seed"])
|
|
539
|
+
split = split_by_availability(mask_0, mask_1)
|
|
540
|
+
complete_idx = split["complete_idx"]
|
|
541
|
+
|
|
542
|
+
X_views_t = [torch.tensor(X, dtype=torch.float32) for X in X_views]
|
|
543
|
+
|
|
544
|
+
# ---- one affinity graph W(v)/L(v) PER VIEW, built from that view's own
|
|
545
|
+
# complete samples X(v)_C (Eq. 2, Section 3.2) ----
|
|
546
|
+
graphs = [build_view_graph(X[complete_idx], settings["p_neighbors"]) for X in X_views]
|
|
547
|
+
|
|
548
|
+
# ---- Algorithm 1, line 1: pretrain view-specific DNNs by auto-encoding ----
|
|
549
|
+
encoders, decoders = _build_and_pretrain_networks(settings, X_views, X_views_t)
|
|
550
|
+
|
|
551
|
+
# ---- Algorithm 1, lines 2-3: initialize U(v), PC (Eq. 7) and P(v)_I randomly ----
|
|
552
|
+
U_list, PC, PI_list = _initialize_factors(settings, split, rng)
|
|
553
|
+
|
|
554
|
+
alpha_list = [settings["alpha_1"], settings["alpha_2"]]
|
|
555
|
+
|
|
556
|
+
# NMF-only warm-up on the complete samples to minimize Eq. (7) before joint training
|
|
557
|
+
U_list, PC = _nmf_warmup(settings, encoders, X_views_t, split, graphs, U_list, PC, alpha_list)
|
|
558
|
+
|
|
559
|
+
# ---- Algorithm 1, lines 4-9: alternating optimization until convergence ----
|
|
560
|
+
U_list, PC, PI_list = train_dimc(
|
|
561
|
+
X_views_t, encoders, U_list, PC, PI_list, split, graphs, alpha_list,
|
|
562
|
+
learning_rate=settings["learning_rate"],
|
|
563
|
+
n_epochs=settings["n_epochs"],
|
|
564
|
+
log_every=settings["log_every"],
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
# ---- assemble shared representation P and cluster (Section 3, end) ----
|
|
568
|
+
P_shared = _assemble_shared_representation(settings, split, n_samples, PC, PI_list)
|
|
569
|
+
|
|
570
|
+
cluster_labels = cluster_representation(P_shared, settings["n_clusters"], seed)
|
|
571
|
+
|
|
572
|
+
acc = compute_ACC(labels, cluster_labels)
|
|
573
|
+
nmi = compute_NMI(labels, cluster_labels)
|
|
574
|
+
runtime = time.time() - t0
|
|
575
|
+
|
|
576
|
+
print(f"ACC={acc:.4f} NMI={nmi:.4f} Runtime={runtime:.2f}s")
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
"ACC": acc,
|
|
580
|
+
"NMI": nmi,
|
|
581
|
+
"encoders": encoders,
|
|
582
|
+
"decoders": decoders,
|
|
583
|
+
"U_list": U_list,
|
|
584
|
+
"PC": PC,
|
|
585
|
+
"PI_list": PI_list,
|
|
586
|
+
"P_shared": P_shared,
|
|
587
|
+
"cluster_labels": cluster_labels,
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
# ============================================================
|
|
592
|
+
# 9b. Parameter sensitivity sweep for alpha(1)/alpha(2) (Section 4.4, Fig. 4)
|
|
593
|
+
# ============================================================
|
|
594
|
+
# The paper does NOT use one fixed alpha across datasets: Section 4.4 explicitly
|
|
595
|
+
# sweeps alpha(1)/alpha(2) per dataset and picks the values that maximize NMI
|
|
596
|
+
# (Fig. 4 shows NMI rises then falls as alpha grows -- too small under-uses the
|
|
597
|
+
# graph regularizer, too large lets it dominate reconstruction and over-smooth
|
|
598
|
+
# PC, which hurts clustering). alpha=60 is the value the paper found for its
|
|
599
|
+
# own Multiple Features preprocessing; a different fou/pix file (different
|
|
600
|
+
# scaling, different feature statistics) can need a very different alpha.
|
|
601
|
+
# Run this sweep on your own data before trusting a fixed default.
|
|
602
|
+
|
|
603
|
+
def sweep_alpha(
|
|
604
|
+
settings: Dict[str, Any],
|
|
605
|
+
alpha_candidates: Sequence[float],
|
|
606
|
+
X_views: Optional[Sequence[np.ndarray]] = None,
|
|
607
|
+
labels: Optional[np.ndarray] = None,
|
|
608
|
+
) -> List[Dict[str, float]]:
|
|
609
|
+
"""
|
|
610
|
+
Try each alpha value (same alpha for both views, as in Fig. 4) with a
|
|
611
|
+
reduced epoch budget, and report ACC/NMI for each so you can pick the
|
|
612
|
+
best one for your data before doing a full run.
|
|
613
|
+
|
|
614
|
+
Parameters
|
|
615
|
+
----------
|
|
616
|
+
settings : Dict[str, Any]
|
|
617
|
+
Base settings dictionary; a copy is modified per candidate alpha.
|
|
618
|
+
alpha_candidates : Sequence[float]
|
|
619
|
+
Candidate values to try for both ``alpha_1`` and ``alpha_2``.
|
|
620
|
+
X_views : Optional[Sequence[numpy.ndarray]], default=None
|
|
621
|
+
Pre-loaded views, forwarded to ``run_dimc``.
|
|
622
|
+
labels : Optional[numpy.ndarray], default=None
|
|
623
|
+
Ground-truth labels, forwarded to ``run_dimc``.
|
|
624
|
+
|
|
625
|
+
Returns
|
|
626
|
+
-------
|
|
627
|
+
List[Dict[str, float]]
|
|
628
|
+
One dict per candidate alpha with keys ``"alpha"``, ``"ACC"``,
|
|
629
|
+
and ``"NMI"``.
|
|
630
|
+
"""
|
|
631
|
+
sweep_settings = dict(settings)
|
|
632
|
+
sweep_settings["n_epochs"] = min(settings["n_epochs"], SWEEP_MAX_EPOCHS)
|
|
633
|
+
sweep_settings["log_every"] = sweep_settings["n_epochs"] # only log the final epoch
|
|
634
|
+
|
|
635
|
+
scores = []
|
|
636
|
+
for alpha in alpha_candidates:
|
|
637
|
+
sweep_settings["alpha_1"] = alpha
|
|
638
|
+
sweep_settings["alpha_2"] = alpha
|
|
639
|
+
result = run_dimc(sweep_settings, X_views=X_views, labels=labels)
|
|
640
|
+
scores.append({"alpha": alpha, "ACC": result["ACC"], "NMI": result["NMI"]})
|
|
641
|
+
print(f"alpha={alpha:<8} -> ACC={result['ACC']:.4f} NMI={result['NMI']:.4f}")
|
|
642
|
+
|
|
643
|
+
best = max(scores, key=lambda s: s["NMI"])
|
|
644
|
+
print(f"\nBest alpha by NMI: {best['alpha']} (ACC={best['ACC']:.4f}, NMI={best['NMI']:.4f})")
|
|
645
|
+
return scores
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def fit(
|
|
649
|
+
X_views: Optional[Sequence[np.ndarray]] = None,
|
|
650
|
+
labels: Optional[np.ndarray] = None,
|
|
651
|
+
**overrides: Any,
|
|
652
|
+
) -> Dict[str, Any]:
|
|
653
|
+
"""
|
|
654
|
+
Convenience entry point: run the full DIMC pipeline using
|
|
655
|
+
``dimc.config.DEFAULT_SETTINGS`` merged with any keyword overrides.
|
|
656
|
+
|
|
657
|
+
This matches the requested top-level usage::
|
|
658
|
+
|
|
659
|
+
from dimc import fit
|
|
660
|
+
result = fit(X_views, labels, learning_rate=1e-5, alpha_1=60, alpha_2=60)
|
|
661
|
+
|
|
662
|
+
Parameters
|
|
663
|
+
----------
|
|
664
|
+
X_views : Optional[Sequence[numpy.ndarray]], default=None
|
|
665
|
+
Pre-loaded views. If ``None``, views are loaded from the default
|
|
666
|
+
(or overridden) ``fou_path`` / ``pix_path`` settings.
|
|
667
|
+
labels : Optional[numpy.ndarray], default=None
|
|
668
|
+
Ground-truth labels. If ``None``, labels are derived from the
|
|
669
|
+
default (or overridden) ``n_classes`` / ``samples_per_class``
|
|
670
|
+
settings.
|
|
671
|
+
**overrides : Any
|
|
672
|
+
Any key from ``dimc.config.DEFAULT_SETTINGS`` to override, e.g.
|
|
673
|
+
``learning_rate=1e-5``, ``alpha_1=60``, ``alpha_2=60``.
|
|
674
|
+
|
|
675
|
+
Returns
|
|
676
|
+
-------
|
|
677
|
+
Dict[str, Any]
|
|
678
|
+
Same return value as ``run_dimc``.
|
|
679
|
+
"""
|
|
680
|
+
settings = dict(DEFAULT_SETTINGS)
|
|
681
|
+
settings.update(overrides)
|
|
682
|
+
return run_dimc(settings, X_views=X_views, labels=labels)
|