autoencodix 0.2.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.

Potentially problematic release.


This version of autoencodix might be problematic. Click here for more details.

Files changed (91) hide show
  1. autoencodix/__init__.py +27 -0
  2. autoencodix/base/.ipynb_checkpoints/_base_pipeline-checkpoint.py +1274 -0
  3. autoencodix/base/.ipynb_checkpoints/_base_trainer-checkpoint.py +247 -0
  4. autoencodix/base/__init__.py +21 -0
  5. autoencodix/base/_base_autoencoder.py +138 -0
  6. autoencodix/base/_base_dataset.py +103 -0
  7. autoencodix/base/_base_evaluator.py +34 -0
  8. autoencodix/base/_base_loss.py +311 -0
  9. autoencodix/base/_base_pipeline.py +1355 -0
  10. autoencodix/base/_base_predictor.py +6 -0
  11. autoencodix/base/_base_preprocessor.py +1033 -0
  12. autoencodix/base/_base_trainer.py +259 -0
  13. autoencodix/base/_base_visualizer.py +578 -0
  14. autoencodix/base/sampleconfig.yaml +132 -0
  15. autoencodix/configs/__init__.py +21 -0
  16. autoencodix/configs/default_config.py +652 -0
  17. autoencodix/configs/disentanglix_config.py +15 -0
  18. autoencodix/configs/maskix_config.py +39 -0
  19. autoencodix/configs/ontix_config.py +35 -0
  20. autoencodix/configs/stackix_config.py +33 -0
  21. autoencodix/configs/vanillix_config.py +18 -0
  22. autoencodix/configs/varix_config.py +15 -0
  23. autoencodix/configs/xmodalix_config.py +43 -0
  24. autoencodix/disentanglix.py +72 -0
  25. autoencodix/evaluate/.ipynb_checkpoints/evaluate-checkpoint.py +532 -0
  26. autoencodix/evaluate/__init__.py +4 -0
  27. autoencodix/evaluate/_general_evaluator.py +730 -0
  28. autoencodix/evaluate/_xmodalix_evaluator.py +376 -0
  29. autoencodix/imagix.py +76 -0
  30. autoencodix/losses/disentanglix_loss.py +215 -0
  31. autoencodix/losses/maskix_loss.py +94 -0
  32. autoencodix/losses/vanillix_loss.py +28 -0
  33. autoencodix/losses/varix_loss.py +102 -0
  34. autoencodix/losses/xmodal_loss.py +304 -0
  35. autoencodix/maskix.py +108 -0
  36. autoencodix/modeling/.ipynb_checkpoints/__init__-checkpoint.py +16 -0
  37. autoencodix/modeling/.ipynb_checkpoints/_captum_forward-checkpoint.py +16 -0
  38. autoencodix/modeling/__init__.py +20 -0
  39. autoencodix/modeling/_captum_forward.py +20 -0
  40. autoencodix/modeling/_classifier.py +45 -0
  41. autoencodix/modeling/_imagevae_architecture.py +296 -0
  42. autoencodix/modeling/_imgfast_architecture.py +288 -0
  43. autoencodix/modeling/_layer_factory.py +71 -0
  44. autoencodix/modeling/_maskix_architecture.py +190 -0
  45. autoencodix/modeling/_ontix_architecture.py +305 -0
  46. autoencodix/modeling/_vanillix_architecture.py +136 -0
  47. autoencodix/modeling/_varix_architecture.py +192 -0
  48. autoencodix/notebooks/__init__.py +0 -0
  49. autoencodix/ontix.py +136 -0
  50. autoencodix/py.typed +0 -0
  51. autoencodix/stackix.py +122 -0
  52. autoencodix/trainers/__init__.py +6 -0
  53. autoencodix/trainers/_general_trainer.py +525 -0
  54. autoencodix/trainers/_maskix_trainer.py +116 -0
  55. autoencodix/trainers/_ontix_trainer.py +77 -0
  56. autoencodix/trainers/_stackix_orchestrator.py +522 -0
  57. autoencodix/trainers/_stackix_trainer.py +209 -0
  58. autoencodix/trainers/_xmodal_trainer.py +1006 -0
  59. autoencodix/utils/.ipynb_checkpoints/_explainer-checkpoint.py +118 -0
  60. autoencodix/utils/.ipynb_checkpoints/_model_output-checkpoint.py +30 -0
  61. autoencodix/utils/.ipynb_checkpoints/_utils-checkpoint.py +626 -0
  62. autoencodix/utils/.ipynb_checkpoints/adata_converter-checkpoint.py +99 -0
  63. autoencodix/utils/.ipynb_checkpoints/feature_importance-checkpoint.py +823 -0
  64. autoencodix/utils/__init__.py +22 -0
  65. autoencodix/utils/_annealer.py +193 -0
  66. autoencodix/utils/_bulkreader.py +232 -0
  67. autoencodix/utils/_explainer.py +246 -0
  68. autoencodix/utils/_imgreader.py +404 -0
  69. autoencodix/utils/_internals.py +23 -0
  70. autoencodix/utils/_llm_explainer.py +318 -0
  71. autoencodix/utils/_losses.py +20 -0
  72. autoencodix/utils/_model_output.py +30 -0
  73. autoencodix/utils/_result.py +393 -0
  74. autoencodix/utils/_screader.py +76 -0
  75. autoencodix/utils/_traindynamics.py +187 -0
  76. autoencodix/utils/_utils.py +690 -0
  77. autoencodix/utils/adata_converter.py +111 -0
  78. autoencodix/utils/example_data.py +398 -0
  79. autoencodix/utils/feature_importance.py +829 -0
  80. autoencodix/utils/prompts.py +21 -0
  81. autoencodix/vanillix.py +141 -0
  82. autoencodix/varix.py +80 -0
  83. autoencodix/visualize/__init__.py +5 -0
  84. autoencodix/visualize/_general_visualizer.py +913 -0
  85. autoencodix/visualize/_imagix_visualizer.py +111 -0
  86. autoencodix/visualize/_xmodal_visualizer.py +841 -0
  87. autoencodix/visualize/visualize.py +1214 -0
  88. autoencodix/xmodalix.py +318 -0
  89. autoencodix-0.2.0.dist-info/METADATA +143 -0
  90. autoencodix-0.2.0.dist-info/RECORD +91 -0
  91. autoencodix-0.2.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,94 @@
1
+ import torch
2
+ from torch.nn.functional import binary_cross_entropy_with_logits as bce_logits
3
+ from torch.nn.functional import mse_loss
4
+ from autoencodix.configs import DefaultConfig
5
+ from autoencodix.base._base_loss import BaseLoss
6
+ from autoencodix.utils._model_output import ModelOutput
7
+ from typing import Tuple, Dict
8
+
9
+
10
+ class MaskixLoss(BaseLoss):
11
+ def __init__(self, config: DefaultConfig, annealing_scheduler=None):
12
+ """Inits MaskixLoss
13
+ Args:
14
+ config: Configuration object.Any
15
+ annealing_scheduler: Enables passing a custom annealer class, defaults to our implementation of an annealer
16
+ """
17
+ super().__init__(config, annealing_scheduler=annealing_scheduler)
18
+ self._validate_loss_config()
19
+
20
+ def _validate_model_output(self, model_output: ModelOutput):
21
+ if model_output.additional_info is None:
22
+ raise ValueError(
23
+ "For Maskix, we need to provide an 'additional_info' attribute in the ModelOutput, this likely went wrong in the architecture's forward method"
24
+ )
25
+ if not isinstance(model_output.additional_info, dict):
26
+ raise TypeError(
27
+ f"The `additional_info` attribute of ModelOutput needs to be of type dict, got {type(model_output.additional_info)}"
28
+ )
29
+ if "predicted_mask" not in model_output.additional_info.keys():
30
+ raise ValueError(
31
+ f"For Maskix, we require 'predicted_mask' to be in the additional_info attribute of ModelOutput, got: {model_output.additional_info.keys()}"
32
+ )
33
+
34
+ def _validate_loss_config(self):
35
+ if not self.config.loss_reduction == "mean":
36
+ import warnings
37
+
38
+ warnings.warn(
39
+ f"You chose loss reduction: {self.config.loss_reduction}, this deviates from the implementation in the literature for this architecture, the authors used 'mean'"
40
+ )
41
+ if not self.config.reconstruction_loss == "mse":
42
+ import warnings
43
+
44
+ warnings.warn(
45
+ f"You chose {self.config.reconstruction_loss}, however we support only 'mse' for Maskix, we will use 'mse'"
46
+ )
47
+
48
+ def forward(
49
+ self,
50
+ model_output: ModelOutput,
51
+ targets: torch.Tensor,
52
+ corrupted_input: torch.Tensor,
53
+ **kwargs,
54
+ ) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
55
+ self._validate_model_output(model_output)
56
+ predicted_mask = model_output.additional_info["predicted_mask"] # ty: ignore
57
+
58
+ is_masked = (corrupted_input != targets).float()
59
+ if not predicted_mask.shape == is_masked.shape:
60
+ raise ValueError(
61
+ f"Shape mismatch: predicted_mask {predicted_mask.shape} vs mask {is_masked.shape}"
62
+ )
63
+ mask_loss: torch.Tensor = bce_logits(
64
+ predicted_mask, is_masked, reduction=self.config.loss_reduction
65
+ )
66
+
67
+ # this is from the publication
68
+ # the authors want to give higher incentives to reconstruct corrupted features correctly
69
+ corrupted_weight_matrix: torch.Tensor = (
70
+ self.config.delta_mask_corrupted * is_masked
71
+ + (1 - is_masked) * (1 - self.config.delta_mask_corrupted)
72
+ )
73
+ recon_loss: torch.Tensor = mse_loss(
74
+ model_output.reconstruction, targets, reduction="none"
75
+ )
76
+ recon_loss_weighted = self.reduction_fn(
77
+ torch.mul(corrupted_weight_matrix, recon_loss)
78
+ )
79
+ total_loss: torch.Tensor = (
80
+ mask_loss * self.config.delta_mask_predictor
81
+ + (1 - self.config.delta_mask_predictor) * recon_loss_weighted
82
+ )
83
+
84
+ recon_loss: torch.Tensor = mse_loss(
85
+ model_output.reconstruction, targets, reduction=self.config.loss_reduction
86
+ )
87
+
88
+ # predicted_mask: torch.tensor
89
+ return total_loss, {
90
+ "recon_loss": recon_loss,
91
+ "recon_loss_weighted": (1 - self.config.delta_mask_predictor)
92
+ * recon_loss_weighted,
93
+ "mask_loss": mask_loss * self.config.delta_mask_predictor,
94
+ }
@@ -0,0 +1,28 @@
1
+ import torch
2
+ from typing import Dict, Optional, Tuple
3
+
4
+ from autoencodix.base._base_loss import BaseLoss
5
+ from autoencodix.utils._model_output import ModelOutput
6
+
7
+
8
+ class VanillixLoss(BaseLoss):
9
+ """Implements loss for vanilla autoencoder."""
10
+
11
+ def forward(
12
+ self,
13
+ model_output: ModelOutput,
14
+ targets: torch.Tensor,
15
+ epoch: Optional[int] = None,
16
+ **kwargs,
17
+ ) -> Tuple[torch.Tensor, Dict[str, torch.Tensor]]:
18
+ """Calculates reconstruction loss as specified in config (BCE, MSE, etc.).
19
+
20
+ Args:
21
+ model_output: custom class that stores model output like latentspaces and reconstructions.
22
+ targets: original data to compare with reconstruction
23
+ epoch: not used for Vanillix loss.
24
+ **kwargs: addtional keyword args.
25
+
26
+ """
27
+ total_loss = self.recon_loss(model_output.reconstruction, targets)
28
+ return total_loss, {"recon_loss": total_loss}
@@ -0,0 +1,102 @@
1
+ import torch
2
+ from typing import Tuple, Optional
3
+
4
+ from autoencodix.base._base_loss import BaseLoss
5
+ from autoencodix.utils._model_output import ModelOutput
6
+ from autoencodix.configs.default_config import DefaultConfig
7
+
8
+
9
+ class VarixLoss(BaseLoss):
10
+ """Implements loss for variational autoencoder with unified interface.
11
+ Attributes:
12
+ config: Configuration object
13
+ """
14
+
15
+ def __init__(self, config: DefaultConfig, annealing_scheduler=None):
16
+ """Inits VarixLoss
17
+ Args:
18
+ config: Configuraion object.Any
19
+ annealing_scheduler: Enables passing a custom annealer class, defaults to our implementation of an annealer
20
+ """
21
+ super().__init__(config, annealing_scheduler=annealing_scheduler)
22
+
23
+ def _compute_losses(
24
+ self, model_output: ModelOutput, targets: torch.Tensor
25
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
26
+ """Compute reconstruction and variational losses.
27
+
28
+ Args:
29
+ model_output: custom class that stores model output like latentspaces and reconstructions.
30
+ targets: original data to compare with reconstruction
31
+
32
+ Returns:
33
+ Tuple of torch.Tensors: reconstruction loss and variational loss
34
+
35
+ """
36
+ true_samples = torch.randn(
37
+ self.config.batch_size, self.config.latent_dim, requires_grad=False
38
+ )
39
+
40
+ recon_loss = self.recon_loss(model_output.reconstruction, targets)
41
+ var_loss = self.compute_variational_loss(
42
+ mu=model_output.latent_mean,
43
+ logvar=model_output.latent_logvar,
44
+ z=model_output.latentspace,
45
+ true_samples=true_samples,
46
+ )
47
+
48
+ return recon_loss, var_loss
49
+
50
+ def forward(
51
+ self,
52
+ model_output: ModelOutput,
53
+ targets: torch.Tensor,
54
+ epoch: Optional[int] = None,
55
+ total_epochs: Optional[int] = None,
56
+ **kwargs,
57
+ ) -> Tuple[torch.Tensor, dict]:
58
+ """Forward pass with conditional annealing.
59
+ Args:
60
+ model_output: custom class that stores model output like latentspaces and reconstructions.
61
+ targets: original data to compare with reconstruction
62
+ epoch: current training epoch
63
+ total_epochs: number of total epochs
64
+ **kwargs
65
+ Returns:
66
+ Tuple consisting of:
67
+ - tensor of the total loss
68
+ Returns:
69
+ Tuple consisting of:
70
+ - tensor of the total loss
71
+ - Dict with loss_type as key and sub_loss value.
72
+ - Dict with loss_type as key and sub_loss value.
73
+
74
+ """
75
+
76
+ recon_loss, var_loss = self._compute_losses(model_output, targets)
77
+
78
+ # if are pretraining, we pass total_epochs, otherwise, we use 'epochs' from config
79
+ calc_epochs: int = self.config.epochs
80
+ if total_epochs:
81
+ calc_epochs = total_epochs
82
+
83
+ if self.config.anneal_function == "no-annealing":
84
+ # Use constant beta
85
+ effective_beta = self.config.beta
86
+ anneal_factor = 1.0
87
+ else:
88
+ anneal_factor = self.annealing_scheduler.get_weight(
89
+ epoch_current=epoch,
90
+ total_epoch=calc_epochs,
91
+ func=self.config.anneal_function,
92
+ )
93
+ effective_beta = self.config.beta * anneal_factor
94
+
95
+ total_loss = recon_loss + effective_beta * var_loss
96
+
97
+ return total_loss, {
98
+ "recon_loss": recon_loss,
99
+ "var_loss": var_loss * effective_beta,
100
+ "anneal_factor": torch.tensor(anneal_factor),
101
+ "effective_beta_factor": torch.tensor(effective_beta),
102
+ }
@@ -0,0 +1,304 @@
1
+ import torch
2
+ import numpy as np
3
+ import pandas as pd
4
+ from typing import Dict, Optional, Any, List
5
+ from collections import defaultdict
6
+ from autoencodix.base._base_loss import BaseLoss
7
+ from autoencodix.utils._annealer import AnnealingScheduler
8
+ from autoencodix.configs.default_config import DefaultConfig
9
+ from autoencodix.utils._utils import flip_labels
10
+
11
+
12
+ class XModalLoss(BaseLoss):
13
+ """Implements Loss for XModalix.
14
+ The loss of the XModalix consists of 4 parts:
15
+ - Combined (mean) Reconstruction loss over all sub modalities.
16
+ - Combined Distribution loss over all sub modalities (KL or MMD)
17
+ - Class loss: When we have metadata information about a sample e.g.
18
+ cancer type, we calculate the mean of all samples in the latent
19
+ space of each class and then we calc the distance between the mean
20
+ individual sample.
21
+ - Paired loss: When samples of different modalities are paired, then
22
+ their latens space representation should be proximal.
23
+ - Advers loss: Forces the latentspaaces of different data modalities to
24
+ be similar.
25
+ The reconstruction and distribution loss are calculated, combined, and weighted
26
+ (with hyperparam beta) for each sub-modality first. In this class we grab this
27
+ loss from the modality_dynamics and combine them for all sub-modalities with the mean.
28
+ Attributes:
29
+ class_means_train:
30
+ class_means_valid:
31
+ sample_to_class_map:
32
+ """
33
+
34
+ def __init__(self, config: DefaultConfig, annealing_scheduler=None):
35
+ super().__init__(config)
36
+ self.class_means_train: Dict[str, torch.Tensor] = {}
37
+ self.class_means_valid: Dict[str, torch.Tensor] = {}
38
+ self.sample_to_class_map: Dict[str, Any] = {}
39
+ self.class_mean_momentum = 0.75
40
+
41
+ def forward(
42
+ self,
43
+ batch: Dict[str, Dict[str, Any]],
44
+ modality_dynamics: Dict[str, Dict[str, Any]],
45
+ clf_scores: torch.Tensor,
46
+ labels: torch.Tensor,
47
+ clf_loss_fn: torch.nn.Module,
48
+ is_training: bool = True,
49
+ epoch: Optional[int] = None,
50
+ **kwargs,
51
+ ):
52
+ """Forward pass of XModal loss.
53
+ Args:
54
+ batch: data from custom dataset
55
+ modality_dynamics: trainingdynamics such as losses, reconstructions for each data modality.
56
+ clf_scores: output of latent classifier for advers loss.
57
+ labels: indicator to which datamodality which latentspace belongs (for adver. loss).
58
+ clf_loss_fn: loss for clf, passed in xmodal_trainer.py, defaults Crossentropy.
59
+ is_training: indicator if we're in training, false for valid and test loop. Used for class loss calc.
60
+ **kwargs: addtional keyword arguments.
61
+ """
62
+ adver_loss = self._calc_adversial_loss(
63
+ labels=labels, clf_loss_fn=clf_loss_fn, clf_scores=clf_scores
64
+ )
65
+ aggregated_sub_losses = self._combine_sub_losses(
66
+ modality_dynamics=modality_dynamics
67
+ )
68
+ sub_losses = self._store_sub_losses(modality_dynamics=modality_dynamics)
69
+ paired_loss = self._calc_paired_loss(
70
+ batch=batch, modality_dynamics=modality_dynamics
71
+ )
72
+ class_loss = self._calc_class_loss(
73
+ batch=batch,
74
+ modality_dynamics=modality_dynamics,
75
+ is_training=is_training,
76
+ )
77
+ total_loss = (
78
+ self.config.gamma * adver_loss
79
+ + aggregated_sub_losses # beta already applied when calcing sub_loss in train loop (works because of distributive law.)
80
+ + self.config.delta_pair * paired_loss
81
+ + self.config.delta_class * class_loss
82
+ )
83
+ loss_dict = {
84
+ "adver_loss": self.config.gamma * adver_loss,
85
+ "aggregated_sub_losses": aggregated_sub_losses,
86
+ "paired_loss": self.config.delta_pair * paired_loss,
87
+ "class_loss": self.config.delta_class * class_loss,
88
+ }
89
+ loss_dict.update(sub_losses)
90
+ return total_loss, loss_dict
91
+
92
+ def _calc_class_loss(
93
+ self,
94
+ batch: Dict[str, Dict[str, Any]],
95
+ modality_dynamics: Dict[str, Dict[str, Any]],
96
+ is_training: bool,
97
+ ) -> torch.Tensor:
98
+ """
99
+ Optimized, vectorized version of the original _calc_class_loss.
100
+
101
+ Notes:
102
+ - Handles arbitrary (hashable) class labels (e.g., strings or ints).
103
+ - Groups latents by label across modalities without looping over samples.
104
+ - Initializes new class means from the batch means (detached).
105
+ - Updates existing class means using in-place EMA under torch.no_grad().
106
+ - Preserves original return semantics (avg across modalities that have metadata).
107
+ """
108
+
109
+ if not self.config.class_param:
110
+ # choose a device if possible
111
+ first_mp = next(iter(modality_dynamics.values()), None)
112
+ device = (
113
+ first_mp["mp"].latentspace.device
114
+ if first_mp is not None
115
+ else torch.device("cpu")
116
+ )
117
+ return torch.tensor(0.0, device=device)
118
+
119
+ any_mp = next(iter(modality_dynamics.values()))
120
+ device = any_mp["mp"].latentspace.device
121
+
122
+ class_means_dict = (
123
+ self.class_means_train if is_training else self.class_means_valid
124
+ )
125
+
126
+ # 1) Build global lists: all_latents (tensor), all_labels (python list), and modality slices
127
+ latents_list = []
128
+ labels_list = [] # python objects (strings/ints)
129
+ modality_slices = {} # mod_name -> (start_idx, end_idx)
130
+ start = 0
131
+
132
+ for mod_name, mod_data in batch.items():
133
+ sample_ids: Optional[List[str]] = mod_data.get("sample_ids")
134
+ mod_labels: Optional[List[str]] = mod_data.get("class_labels")
135
+ if not sample_ids or not mod_labels:
136
+ import warnings
137
+
138
+ warnings.warn(f"No metadata for modality {mod_name}")
139
+ continue
140
+ self.sample_to_class_map.update(
141
+ {
142
+ sample_id: mod_label
143
+ for sample_id, mod_label in zip(sample_ids, mod_labels)
144
+ }
145
+ )
146
+ latents = modality_dynamics[mod_name]["mp"].latentspace # (N_mod, D)
147
+ n_mod = latents.shape[0]
148
+
149
+ if len(mod_labels) != n_mod:
150
+ raise ValueError(
151
+ f"Mismatch between number of latents ({n_mod}) and labels ({len(mod_labels)}) for modality {mod_name}"
152
+ )
153
+
154
+ latents_list.append(latents)
155
+ labels_list.extend(mod_labels)
156
+ modality_slices[mod_name] = (start, start + n_mod)
157
+ start += n_mod
158
+
159
+ if len(latents_list) == 0:
160
+ return torch.tensor(0.0, device=device)
161
+ all_latents = torch.cat(latents_list, dim=0) # shape (N_total, D)
162
+
163
+ # 2) Determine unique labels (preserve order) - small (<=20) so Python-level unique is fine
164
+ unique_labels = list(
165
+ dict.fromkeys(labels_list)
166
+ ) # preserves first-occurrence order
167
+
168
+ # 3) Compute batch means per label (vectorized per label w/o per-sample loops)
169
+ batch_means: Dict[Any, torch.Tensor] = {}
170
+ for lbl in unique_labels:
171
+ # find indices for this label (python-level)
172
+ # create a tensor of indices for selection to avoid many small GPU->CPU ops
173
+ indices = [i for i, l in enumerate(labels_list) if l == lbl]
174
+ if len(indices) == 0:
175
+ continue
176
+ idx_tensor = torch.tensor(indices, device=device, dtype=torch.long)
177
+ # gather latents and compute mean
178
+ lbl_latents = all_latents.index_select(0, idx_tensor)
179
+ batch_mean = lbl_latents.mean(dim=0)
180
+ batch_means[lbl] = batch_mean # kept on device
181
+
182
+ # 4) Initialize new classes if needed (mean of current batch samples)
183
+ for lbl, bmean in batch_means.items():
184
+ if lbl not in class_means_dict:
185
+ # store detached clone to avoid accidental linkage to computation graph
186
+ class_means_dict[lbl] = bmean.detach().clone()
187
+
188
+ # 5) Compute loss per modality using current class means
189
+ total_class_loss = torch.tensor(0.0, device=device)
190
+ num_modalities_with_metadata = 0
191
+
192
+ for mod_name, (start_idx, end_idx) in modality_slices.items():
193
+ num_modalities_with_metadata += 1
194
+ latents = all_latents[start_idx:end_idx] # view into concatenated tensor
195
+ mod_labels = labels_list[start_idx:end_idx]
196
+
197
+ # Build target_means_tensor by stacking class_means_dict entries (ensures device alignment)
198
+ # This is vectorized: (N_mod, D)
199
+ target_means = torch.stack(
200
+ [class_means_dict[lbl].to(device) for lbl in mod_labels], dim=0
201
+ )
202
+
203
+ distances = torch.linalg.norm(latents - target_means, dim=1)
204
+
205
+ # Apply the configured reduction to distances
206
+ total_class_loss = total_class_loss + self.reduction_fn(distances)
207
+
208
+ # Average across modalities (same semantic as original)
209
+ avg_class_loss = (
210
+ total_class_loss / num_modalities_with_metadata
211
+ if num_modalities_with_metadata > 0
212
+ else total_class_loss
213
+ )
214
+
215
+ # 6) If training, update class means using EMA with batch statistics (in-place, no_grad)
216
+ # if is_training:
217
+ with torch.no_grad():
218
+ for lbl, bmean in batch_means.items():
219
+ if lbl in class_means_dict:
220
+ # ensure same device, then update in-place
221
+ cm = class_means_dict[lbl]
222
+ if cm.device != bmean.device:
223
+ cm = cm.to(bmean.device)
224
+ class_means_dict[lbl] = cm
225
+ cm.mul_(self.class_mean_momentum).add_(
226
+ bmean, alpha=(1 - self.class_mean_momentum)
227
+ )
228
+ else:
229
+ # this branch shouldn't run due to initialization above, but keep for safety
230
+ class_means_dict[lbl] = bmean.detach().clone()
231
+
232
+ return avg_class_loss
233
+
234
+ def _calc_paired_loss(
235
+ self,
236
+ batch: Dict[str, Dict[str, Any]],
237
+ modality_dynamics: Dict[str, Dict[str, Any]],
238
+ ) -> torch.Tensor:
239
+ """Compute the paired loss across modalities in the current batch.
240
+ This method prepares latent spaces and sample IDs for each modality and
241
+ computes a paired loss if at least two modalities are present. If fewer
242
+ than two modalities exist, it returns a zero tensor that still requires
243
+ gradients to preserve the computation graph.
244
+ Args:
245
+ batch: A dictionary mapping modality names to modality data.
246
+ Each entry contains sample identifiers under the key
247
+ `"sample_ids"`.
248
+ modality_dynamics: A dictionary mapping modality names to their
249
+ dynamics, where each entry contains a `"mp"` object with a
250
+ `latentspace` tensor.
251
+ Returns:
252
+ A scalar tensor representing the paired loss. Returns a zero tensor
253
+ requiring gradients if fewer than two modalities are available.
254
+ """
255
+ latentspaces = {
256
+ mod_name: dynamics["mp"].latentspace
257
+ for mod_name, dynamics in modality_dynamics.items()
258
+ }
259
+ sample_ids = {
260
+ mod_name: mod_data["sample_ids"] for mod_name, mod_data in batch.items()
261
+ }
262
+ if len(latentspaces) < 2:
263
+ # Return a zero tensor that requires gradients to avoid issues in the graph
264
+ # Assuming at least one tensor exists to get the device
265
+ any_latent_tensor = next(iter(latentspaces.values()))
266
+ return torch.tensor(
267
+ 0.0, device=any_latent_tensor.device, requires_grad=True
268
+ )
269
+ return self.compute_paired_loss(
270
+ latentspaces=latentspaces, sample_ids=sample_ids
271
+ )
272
+
273
+ def _combine_sub_losses(
274
+ self, modality_dynamics: Dict[str, Dict[str, Any]]
275
+ ) -> torch.Tensor:
276
+ """Combines the sub losses total loss for all modalities."""
277
+ losses = [helper["loss"] for helper in modality_dynamics.values()]
278
+ if self.config.loss_reduction == "mean":
279
+ return torch.stack(losses).mean()
280
+ return torch.stack(losses).sum()
281
+
282
+ def _store_sub_losses(
283
+ self, modality_dynamics: Dict[str, Dict[str, Any]]
284
+ ) -> Dict[str, float]:
285
+ sub_losses = {}
286
+ for k, v in modality_dynamics.items():
287
+ for k2, v2 in v.items():
288
+ if isinstance(v2, dict):
289
+ for k3, v3 in v2.items():
290
+ new_key = f"{k}.{k3}"
291
+ sub_losses[new_key] = v3.item()
292
+ if k2 == "loss":
293
+ sub_losses[f"{k}.loss"] = v2.item()
294
+ return sub_losses
295
+
296
+ def _calc_adversial_loss(
297
+ self,
298
+ labels: torch.Tensor,
299
+ clf_scores: torch.Tensor,
300
+ clf_loss_fn: torch.nn.Module,
301
+ ):
302
+ flipped_labels = flip_labels(labels=labels)
303
+ adversarial_loss = clf_loss_fn(clf_scores, flipped_labels)
304
+ return adversarial_loss
autoencodix/maskix.py ADDED
@@ -0,0 +1,108 @@
1
+ from typing import Dict, Optional, Type, Union, Callable, Any
2
+ import numpy as np
3
+ import torch
4
+
5
+ from autoencodix.base._base_dataset import BaseDataset
6
+ from autoencodix.base._base_loss import BaseLoss
7
+ from autoencodix.base._base_pipeline import BasePipeline
8
+ from autoencodix.base._base_trainer import BaseTrainer
9
+ from autoencodix.base._base_visualizer import BaseVisualizer
10
+ from autoencodix.base._base_preprocessor import BasePreprocessor
11
+ from autoencodix.base._base_autoencoder import BaseAutoencoder
12
+ from autoencodix.base._base_evaluator import BaseEvaluator
13
+ from autoencodix.data._datasetcontainer import DatasetContainer
14
+ from autoencodix.data._datasplitter import DataSplitter
15
+ from autoencodix.data.datapackage import DataPackage
16
+ from autoencodix.data._numeric_dataset import NumericDataset
17
+ from autoencodix.data.general_preprocessor import GeneralPreprocessor
18
+ from autoencodix.evaluate._general_evaluator import GeneralEvaluator
19
+
20
+ from autoencodix.utils._result import Result
21
+ from autoencodix.configs.default_config import DefaultConfig
22
+ from autoencodix.configs.maskix_config import MaskixConfig
23
+ from autoencodix.utils._losses import MaskixLoss
24
+ from autoencodix.modeling._maskix_architecture import MaskixArchitectureVanilla
25
+ from autoencodix.trainers._maskix_trainer import MaskixTrainer
26
+ from autoencodix.visualize._general_visualizer import GeneralVisualizer
27
+ from autoencodix.utils._model_output import ModelOutput
28
+
29
+
30
+ class Maskix(BasePipeline):
31
+ """Maskix specific version of the BasePipeline class.
32
+
33
+ Inherits preprocess, fit, predict, evaluate, and visualize methods from BasePipeline.
34
+
35
+ This class extends BasePipeline. See the parent class for a full list
36
+ of attributes and methods.
37
+
38
+ Additional Attributes:
39
+ _default_config: Is set to OntixConfig here.
40
+
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ data: Optional[Union[DataPackage, DatasetContainer]] = None,
46
+ trainer_type: Type[BaseTrainer] = MaskixTrainer,
47
+ dataset_type: Type[BaseDataset] = NumericDataset,
48
+ model_type: Type[BaseAutoencoder] = MaskixArchitectureVanilla,
49
+ loss_type: Type[BaseLoss] = MaskixLoss,
50
+ preprocessor_type: Type[BasePreprocessor] = GeneralPreprocessor,
51
+ visualizer: Type[BaseVisualizer] = GeneralVisualizer,
52
+ evaluator: Optional[Type[BaseEvaluator]] = GeneralEvaluator,
53
+ result: Optional[Result] = None,
54
+ datasplitter_type: Type[DataSplitter] = DataSplitter,
55
+ custom_splits: Optional[Dict[str, np.ndarray]] = None,
56
+ config: Optional[DefaultConfig] = None,
57
+ masking_fn: Optional[Callable] = None,
58
+ masking_fn_kwargs: Dict[str, Any] = {},
59
+ **kwargs: dict,
60
+ ) -> None:
61
+ """Initialize Maskix pipeline with customizable components.
62
+
63
+ Some components are passed as types rather than instances because they require
64
+ data that is only available after preprocessing.
65
+
66
+ See parent class for full list of Arguments.
67
+
68
+ Raises:
69
+ TypeError: if ontologies are not a Tuple or List.
70
+
71
+ """
72
+ self._default_config = MaskixConfig()
73
+
74
+ super().__init__(
75
+ data=data,
76
+ dataset_type=dataset_type,
77
+ trainer_type=trainer_type,
78
+ model_type=model_type,
79
+ loss_type=loss_type,
80
+ preprocessor_type=preprocessor_type,
81
+ visualizer=visualizer,
82
+ evaluator=evaluator,
83
+ result=result,
84
+ datasplitter_type=datasplitter_type,
85
+ config=config,
86
+ custom_split=custom_splits,
87
+ masking_fn=masking_fn,
88
+ masking_fn_kwargs=masking_fn_kwargs,
89
+ **kwargs,
90
+ )
91
+
92
+ def impute(self, corrupted_tensor: torch.Tensor) -> ModelOutput:
93
+ """Impute missing values in the corrupted tensor using the trained Maskix model.
94
+
95
+ Args:
96
+ corrupted_tensor: A tensor with missing values to be imputed.
97
+ Returns:
98
+ model_output: A tensor with imputed values.
99
+ """
100
+ if self._trainer is None:
101
+ raise ValueError(
102
+ "Model needs to be trained before imputation., Run fit() first."
103
+ )
104
+ self._trainer._model.eval()
105
+ with torch.no_grad(), self._trainer._fabric.autocast():
106
+ corrupted_tensor = corrupted_tensor.to(self._trainer._model.device)
107
+ model_output = self._trainer._model(corrupted_tensor)
108
+ return model_output
@@ -0,0 +1,16 @@
1
+ from ._layer_factory import LayerFactory
2
+ from ._vanillix_architecture import VanillixArchitecture
3
+ from ._varix_architecture import VarixArchitecture
4
+ from ._ontix_architecture import OntixArchitecture
5
+ from ._imagevae_architecture import ImageVAEArchitecture
6
+ from ._classifier import Classifier
7
+
8
+ # test done
9
+ __all__ = [
10
+ "LayerFactory",
11
+ "VanillixArchitecture",
12
+ "VarixArchitecture",
13
+ "OntixArchitecture",
14
+ "ImageVAEArchitecture",
15
+ "Classifier",
16
+ ]
@@ -0,0 +1,16 @@
1
+ import torch
2
+ import torch.nn as nn
3
+ from autoencodix.base._base_autoencoder import BaseAutoencoder
4
+ from autoencodix.utils._model_output import ModelOutput
5
+
6
+
7
+ class CaptumForward(nn.Module):
8
+ def __init__(self, model: BaseAutoencoder, dim: int):
9
+ super(CaptumForward, self).__init__() # <-- REQUIRED
10
+ self.model = model # (Registered as a submodule)
11
+ self.dim = dim
12
+ def forward(self, x: torch.Tensor):
13
+ mp: ModelOutput = self.model(x=x)
14
+ latent = mp.latentspace
15
+ output = latent[:, self.dim]
16
+ return output.unsqueeze(1)