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.
- autoencodix/__init__.py +27 -0
- autoencodix/base/.ipynb_checkpoints/_base_pipeline-checkpoint.py +1274 -0
- autoencodix/base/.ipynb_checkpoints/_base_trainer-checkpoint.py +247 -0
- autoencodix/base/__init__.py +21 -0
- autoencodix/base/_base_autoencoder.py +138 -0
- autoencodix/base/_base_dataset.py +103 -0
- autoencodix/base/_base_evaluator.py +34 -0
- autoencodix/base/_base_loss.py +311 -0
- autoencodix/base/_base_pipeline.py +1355 -0
- autoencodix/base/_base_predictor.py +6 -0
- autoencodix/base/_base_preprocessor.py +1033 -0
- autoencodix/base/_base_trainer.py +259 -0
- autoencodix/base/_base_visualizer.py +578 -0
- autoencodix/base/sampleconfig.yaml +132 -0
- autoencodix/configs/__init__.py +21 -0
- autoencodix/configs/default_config.py +652 -0
- autoencodix/configs/disentanglix_config.py +15 -0
- autoencodix/configs/maskix_config.py +39 -0
- autoencodix/configs/ontix_config.py +35 -0
- autoencodix/configs/stackix_config.py +33 -0
- autoencodix/configs/vanillix_config.py +18 -0
- autoencodix/configs/varix_config.py +15 -0
- autoencodix/configs/xmodalix_config.py +43 -0
- autoencodix/disentanglix.py +72 -0
- autoencodix/evaluate/.ipynb_checkpoints/evaluate-checkpoint.py +532 -0
- autoencodix/evaluate/__init__.py +4 -0
- autoencodix/evaluate/_general_evaluator.py +730 -0
- autoencodix/evaluate/_xmodalix_evaluator.py +376 -0
- autoencodix/imagix.py +76 -0
- autoencodix/losses/disentanglix_loss.py +215 -0
- autoencodix/losses/maskix_loss.py +94 -0
- autoencodix/losses/vanillix_loss.py +28 -0
- autoencodix/losses/varix_loss.py +102 -0
- autoencodix/losses/xmodal_loss.py +304 -0
- autoencodix/maskix.py +108 -0
- autoencodix/modeling/.ipynb_checkpoints/__init__-checkpoint.py +16 -0
- autoencodix/modeling/.ipynb_checkpoints/_captum_forward-checkpoint.py +16 -0
- autoencodix/modeling/__init__.py +20 -0
- autoencodix/modeling/_captum_forward.py +20 -0
- autoencodix/modeling/_classifier.py +45 -0
- autoencodix/modeling/_imagevae_architecture.py +296 -0
- autoencodix/modeling/_imgfast_architecture.py +288 -0
- autoencodix/modeling/_layer_factory.py +71 -0
- autoencodix/modeling/_maskix_architecture.py +190 -0
- autoencodix/modeling/_ontix_architecture.py +305 -0
- autoencodix/modeling/_vanillix_architecture.py +136 -0
- autoencodix/modeling/_varix_architecture.py +192 -0
- autoencodix/notebooks/__init__.py +0 -0
- autoencodix/ontix.py +136 -0
- autoencodix/py.typed +0 -0
- autoencodix/stackix.py +122 -0
- autoencodix/trainers/__init__.py +6 -0
- autoencodix/trainers/_general_trainer.py +525 -0
- autoencodix/trainers/_maskix_trainer.py +116 -0
- autoencodix/trainers/_ontix_trainer.py +77 -0
- autoencodix/trainers/_stackix_orchestrator.py +522 -0
- autoencodix/trainers/_stackix_trainer.py +209 -0
- autoencodix/trainers/_xmodal_trainer.py +1006 -0
- autoencodix/utils/.ipynb_checkpoints/_explainer-checkpoint.py +118 -0
- autoencodix/utils/.ipynb_checkpoints/_model_output-checkpoint.py +30 -0
- autoencodix/utils/.ipynb_checkpoints/_utils-checkpoint.py +626 -0
- autoencodix/utils/.ipynb_checkpoints/adata_converter-checkpoint.py +99 -0
- autoencodix/utils/.ipynb_checkpoints/feature_importance-checkpoint.py +823 -0
- autoencodix/utils/__init__.py +22 -0
- autoencodix/utils/_annealer.py +193 -0
- autoencodix/utils/_bulkreader.py +232 -0
- autoencodix/utils/_explainer.py +246 -0
- autoencodix/utils/_imgreader.py +404 -0
- autoencodix/utils/_internals.py +23 -0
- autoencodix/utils/_llm_explainer.py +318 -0
- autoencodix/utils/_losses.py +20 -0
- autoencodix/utils/_model_output.py +30 -0
- autoencodix/utils/_result.py +393 -0
- autoencodix/utils/_screader.py +76 -0
- autoencodix/utils/_traindynamics.py +187 -0
- autoencodix/utils/_utils.py +690 -0
- autoencodix/utils/adata_converter.py +111 -0
- autoencodix/utils/example_data.py +398 -0
- autoencodix/utils/feature_importance.py +829 -0
- autoencodix/utils/prompts.py +21 -0
- autoencodix/vanillix.py +141 -0
- autoencodix/varix.py +80 -0
- autoencodix/visualize/__init__.py +5 -0
- autoencodix/visualize/_general_visualizer.py +913 -0
- autoencodix/visualize/_imagix_visualizer.py +111 -0
- autoencodix/visualize/_xmodal_visualizer.py +841 -0
- autoencodix/visualize/visualize.py +1214 -0
- autoencodix/xmodalix.py +318 -0
- autoencodix-0.2.0.dist-info/METADATA +143 -0
- autoencodix-0.2.0.dist-info/RECORD +91 -0
- 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)
|