bsvae 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
bsvae/__init__.py ADDED
File without changes
bsvae/hyperparam.ini ADDED
@@ -0,0 +1,82 @@
1
+ [Custom]
2
+ # General options
3
+ log_level = "info"
4
+ no_progress_bar = False
5
+ no_cuda = False
6
+ seed = 1234
7
+
8
+ # Training options
9
+ epochs = 100
10
+ batch_size = 64
11
+ lr = 5e-4
12
+ checkpoint_every = 30
13
+ dataset = "genenet"
14
+ experiment = "structured"
15
+
16
+ # Model options
17
+ model = "StructuredFactorVAE"
18
+ latent_dim = 10
19
+ hidden_dims = [512, 256] # encoder hidden layers
20
+ dropout = 0.1
21
+ init_sd = 0.02
22
+ learn_var = False # decoder per-gene variance
23
+
24
+ # Loss options
25
+ loss = "beta"
26
+ beta = 1.0 # KL weight
27
+ l1_strength = 1e-3 # sparsity regularizer
28
+ lap_strength = 1e-4 # Laplacian smoothness
29
+
30
+ # Evaluations
31
+ is_metrics = True
32
+ no_test = False
33
+ is_eval_only = False
34
+ eval_batchsize = 512
35
+
36
+
37
+ # ### DATASET COMMON ###
38
+ [Common_genenet]
39
+ dataset = "genenet"
40
+ checkpoint_every = 100
41
+ epochs = 400
42
+
43
+
44
+ # ### LOSS COMMON ###
45
+ [Common_VAE]
46
+ loss = "VAE"
47
+ lr = 5e-4
48
+ [Common_beta]
49
+ loss = "beta"
50
+ beta = 4.0
51
+ lr = 5e-4
52
+
53
+
54
+ # ### EXPERIMENT SPECIFIC ###
55
+
56
+ [VAE_genenet]
57
+ dataset = "genenet"
58
+ model = "StructuredFactorVAE"
59
+ loss = "VAE"
60
+ latent_dim = 8
61
+ epochs = 400
62
+ checkpoint_every = 50
63
+ lr = 1e-4
64
+ l1_strength = 1e-3
65
+ lap_strength = 1e-4
66
+
67
+ [beta_genenet]
68
+ dataset = "genenet"
69
+ model = "StructuredFactorVAE"
70
+ loss = "beta"
71
+ beta = 4.0
72
+ latent_dim = 8
73
+ epochs = 400
74
+ checkpoint_every = 50
75
+ lr = 1e-4
76
+ l1_strength = 5e-4
77
+ lap_strength = 5e-5
78
+
79
+ [debug]
80
+ epochs = 1
81
+ log_level = "debug"
82
+ no_test = True
bsvae/main.py ADDED
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env python
2
+
3
+ import sys
4
+ import logging
5
+ import argparse
6
+ from os.path import join, dirname
7
+ from configparser import ConfigParser
8
+
9
+ import torch
10
+ from torch import optim
11
+
12
+ from bsvae.models import StructuredFactorVAE
13
+ from bsvae.models.losses import BaseLoss
14
+ from bsvae.utils.datasets import get_dataloaders
15
+ from bsvae.utils.helpers import (
16
+ set_seed,
17
+ get_device,
18
+ get_n_params,
19
+ create_safe_directory,
20
+ FormatterNoDuplicate,
21
+ get_config_section,
22
+ update_namespace_,
23
+ )
24
+ from bsvae.utils import Trainer, Evaluator
25
+ from bsvae.utils.ppi import load_ppi_laplacian
26
+ from bsvae.utils.modelIO import save_model, load_model, load_metadata
27
+
28
+ RES_DIR = "results"
29
+
30
+ def load_config(config_path: str, section: str = "Custom") -> dict:
31
+ """Load hyperparameters from .ini file."""
32
+ parser = ConfigParser()
33
+ parser.read(config_path)
34
+ if section not in parser:
35
+ raise ValueError(f"Config section [{section}] not found in {config_path}")
36
+ return {k: ast_literal_eval(v) for k, v in parser[section].items()}
37
+
38
+
39
+ def ast_literal_eval(val):
40
+ """Helper to parse lists, ints, floats from strings safely."""
41
+ import ast
42
+ try:
43
+ return ast.literal_eval(val)
44
+ except Exception:
45
+ return val
46
+
47
+
48
+ def parse_arguments(cli_args):
49
+ """Parse CLI args, with defaults pulled from hyperparam.ini."""
50
+ pre_parser = argparse.ArgumentParser(add_help=False)
51
+ pre_parser.add_argument("--config", "-c", type=str,
52
+ default=join(dirname(__file__), "hyperparam.ini"),
53
+ help="Path to hyperparam.ini")
54
+ pre_parser.add_argument("--section", type=str, default="Custom",
55
+ help="Section of .ini to load")
56
+ config_args, _ = pre_parser.parse_known_args(cli_args)
57
+
58
+ # Load defaults from config
59
+ config = load_config(config_args.config, config_args.section)
60
+
61
+ parser = argparse.ArgumentParser(
62
+ description="Training and evaluation for StructuredFactorVAE.",
63
+ formatter_class=FormatterNoDuplicate,
64
+ parents=[pre_parser],
65
+ )
66
+
67
+ # General
68
+ parser.add_argument("name", type=str, help="Experiment name.")
69
+ parser.add_argument("--seed", type=int, default=config.get("seed", 13))
70
+ parser.add_argument("--no-cuda", action="store_true",
71
+ default=config.get("no_cuda", False))
72
+
73
+ # Training
74
+ parser.add_argument("--epochs", type=int, default=config.get("epochs", 100))
75
+ parser.add_argument("--batch-size", type=int,
76
+ default=config.get("batch_size", 64))
77
+ parser.add_argument("--lr", type=float, default=config.get("lr", 5e-4))
78
+ parser.add_argument("--checkpoint-every", type=int,
79
+ default=config.get("checkpoint_every", 10))
80
+
81
+ # Model
82
+ parser.add_argument("--latent-dim", "-z", type=int,
83
+ default=config.get("latent_dim", 10))
84
+ parser.add_argument("--hidden-dims", "-Z", type=ast_literal_eval,
85
+ default=config.get("hidden_dims", [128, 64]))
86
+ parser.add_argument("--dropout", type=float,
87
+ default=config.get("dropout", 0.1))
88
+ parser.add_argument("--learn-var", action="store_true",
89
+ default=config.get("learn_var", False))
90
+ parser.add_argument("--init-sd", type=float,
91
+ default=config.get("init_sd", 0.02))
92
+
93
+ # Loss
94
+ parser.add_argument("--loss", type=str, default=config.get("loss", "VAE"),
95
+ choices=["VAE", "beta"])
96
+ parser.add_argument("--beta", type=float, default=config.get("beta", 1.0))
97
+ parser.add_argument("--l1-strength", type=float,
98
+ default=config.get("l1_strength", 1e-3))
99
+ parser.add_argument("--lap-strength", type=float,
100
+ default=config.get("lap_strength", 1e-4))
101
+
102
+ # Evaluation
103
+ parser.add_argument("--is-eval-only", action="store_true",
104
+ default=config.get("is_eval_only"))
105
+ parser.add_argument("--no-test", action="store_true",
106
+ default=config.get("no_test"))
107
+ parser.add_argument("--eval-batchsize", type=int,
108
+ default=config.get("eval_batchsize"))
109
+
110
+ # Dataset
111
+ parser.add_argument("--dataset", type=str,
112
+ default=config.get("dataset", "genenet"))
113
+ parser.add_argument("--gene-expression-filename", type=str,
114
+ default=config.get("gene_expression_filename", None),
115
+ help="CSV with gene expression (genes x samples).")
116
+ parser.add_argument("--gene-expression-dir", type=str,
117
+ default=config.get("gene_expression_dir", None),
118
+ help="Directory with train/test splits (X_train.csv, X_test.csv).")
119
+
120
+ # PPI Priors
121
+ parser.add_argument("--ppi-taxid", type=str, default=config.get("ppi_taxid", "9606"))
122
+ parser.add_argument("--ppi-cache", type=str, default=config.get("ppi_cache", None))
123
+
124
+ args = parser.parse_args(cli_args)
125
+
126
+ # Validate dataset input
127
+ if bool(args.gene_expression_filename) == bool(args.gene_expression_dir):
128
+ parser.error("Specify exactly one of --gene-expression-filename or --gene-expression-dir.")
129
+
130
+ parser.set_defaults(**vars(args))
131
+ return parser.parse_args(cli_args)
132
+
133
+
134
+ def setup_logging():
135
+ log_fmt = "%(asctime)s %(levelname)s: %(message)s"
136
+ logging.basicConfig(level=logging.INFO, format=log_fmt)
137
+ return logging.getLogger(__name__)
138
+
139
+
140
+ def main(args):
141
+ logger = setup_logging()
142
+ set_seed(args.seed)
143
+ device = get_device(use_gpu=not args.no_cuda)
144
+ exp_dir = join(RES_DIR, args.name)
145
+ logger.info(f"Experiment directory: {exp_dir}")
146
+
147
+ # Training
148
+ if not args.is_eval_only:
149
+ create_safe_directory(exp_dir, logger=logger)
150
+
151
+ # Data
152
+ train_loader = get_dataloaders(
153
+ dataset=args.dataset,
154
+ batch_size=args.batch_size,
155
+ logger=logger,
156
+ train=True,
157
+ drop_last=True,
158
+ gene_expression_filename=args.gene_expression_filename,
159
+ gene_expression_dir=args.gene_expression_dir,
160
+ )
161
+ n_genes = train_loader.dataset[0][0].shape[-1]
162
+ logger.info(f"Training dataset size: {len(train_loader.dataset)}")
163
+
164
+ # PPI Laplacian
165
+ try:
166
+ gene_list = getattr(train_loader.dataset, "genes", None)
167
+ if gene_list is not None:
168
+ L, G = load_ppi_laplacian(
169
+ gene_list,
170
+ taxid=args.ppi_taxid,
171
+ min_score=700,
172
+ cache_dir=args.ppi_cache or "~/.bsvae/ppi",
173
+ )
174
+ logger.info(f"PPI Graph loaded: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges")
175
+ else:
176
+ L = None
177
+ except Exception as e:
178
+ logger.warning(f"Could not load PPI Laplacian: {e}")
179
+ L = None
180
+
181
+ # Model
182
+ model = StructuredFactorVAE(
183
+ n_genes=n_genes,
184
+ n_latent=args.latent_dim,
185
+ hidden_dims=args.hidden_dims,
186
+ dropout=args.dropout,
187
+ init_sd=args.init_sd,
188
+ learn_var=args.learn_var,
189
+ L=L
190
+ ).to(device)
191
+ logger.info(f"Model initialized with {n_genes} genes, {args.latent_dim} latent dims")
192
+
193
+ # Loss + optimizer
194
+ optimizer = optim.Adam(model.parameters(), lr=args.lr)
195
+ loss_f = BaseLoss(beta=args.beta,
196
+ l1_strength=args.l1_strength,
197
+ lap_strength=args.lap_strength)
198
+
199
+ trainer = Trainer(
200
+ model, optimizer, loss_f, device=device, logger=logger,
201
+ save_dir=exp_dir, is_progress_bar=not args.no_cuda,
202
+ )
203
+ trainer(train_loader, epochs=int(args.epochs),
204
+ checkpoint_every=int(args.checkpoint_every))
205
+ save_model(trainer.model, exp_dir, metadata=vars(args))
206
+
207
+ # Evaluation
208
+ if not args.no_test:
209
+ model = load_model(exp_dir, is_gpu=not args.no_cuda)
210
+ metadata = load_metadata(exp_dir)
211
+ test_loader = get_dataloaders(
212
+ dataset="genenet",
213
+ batch_size=args.eval_batchsize,
214
+ shuffle=False,
215
+ logger=logger,
216
+ train=False,
217
+ drop_last=True,
218
+ gene_expression_filename=args.gene_expression_filename,
219
+ gene_expression_dir=args.gene_expression_dir,
220
+ )
221
+ loss_f = BaseLoss(beta=args.beta,
222
+ l1_strength=args.l1_strength,
223
+ lap_strength=args.lap_strength)
224
+ evaluator = Evaluator(
225
+ model, loss_f, device=device, logger=logger, save_dir=exp_dir,
226
+ is_progress_bar=not args.no_cuda,
227
+ )
228
+ evaluator(test_loader)
229
+
230
+
231
+ def cli():
232
+ args = parse_arguments(sys.argv[1:])
233
+ main(args)
234
+
235
+
236
+ if __name__ == "__main__":
237
+ cli()
@@ -0,0 +1,4 @@
1
+ from .vae import BaseVAE
2
+ from .encoder import StructuredEncoder
3
+ from .decoder import StructuredDecoder
4
+ from .structured import StructuredFactorVAE
@@ -0,0 +1,90 @@
1
+ """
2
+ Decoder for StructuredFactorVAE
3
+ """
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+
8
+ class StructuredDecoder(nn.Module):
9
+ """
10
+ Structured decoder for gene expression:
11
+ - Each latent factor corresponds to a module.
12
+ - Decoder weights are optionally masked by prior gene×module assignments.
13
+ - Supports sparsity and Laplacian penalties.
14
+ - Outputs mean and dispersion for Negative Binomial likelihood.
15
+
16
+ Parameters
17
+ ----------
18
+ n_genes : int
19
+ Number of genes (G).
20
+ n_latent : int
21
+ Number of latent factors/modules (K).
22
+ mask : torch.Tensor or None
23
+ Optional binary mask of shape (G, K) where 1 means gene↔factor allowed.
24
+ init_sd : float
25
+ Std for normal init of decoder weights.
26
+ """
27
+ def __init__(self, n_genes: int, n_latent: int,
28
+ mask: torch.Tensor = None,
29
+ init_sd: float = 0.02,
30
+ learn_var: bool = False):
31
+ super().__init__()
32
+ self.n_genes = n_genes
33
+ self.n_latent = n_latent
34
+ self.learn_var = learn_var
35
+
36
+ # Weight matrix W: (G, K)
37
+ self.W = nn.Parameter(torch.randn(n_genes, n_latent) * init_sd)
38
+ self.bias = nn.Parameter(torch.zeros(n_genes))
39
+
40
+ # Mask (non-trainable)
41
+ if mask is not None:
42
+ assert mask.shape == (n_genes, n_latent)
43
+ self.register_buffer("mask", mask.float())
44
+ else:
45
+ self.mask = None
46
+
47
+ # Gene-specific variance (optional)
48
+ if self.learn_var:
49
+ self.log_var = nn.Parameter(torch.zeros(n_genes))
50
+ else:
51
+ self.log_var = None
52
+
53
+ def forward(self, z: torch.Tensor):
54
+ """
55
+ Parameters
56
+ ----------
57
+ z : torch.Tensor
58
+ Latent codes, shape (batch, K).
59
+
60
+ Returns
61
+ -------
62
+ recon_x : torch.Tensor
63
+ Reconstructed gene expression (batch, G).
64
+ log_var : torch.Tensor
65
+ Gene-specific log-variance (G,).
66
+ """
67
+ # Apply mask if provided
68
+ W_eff = self.W
69
+ if self.mask is not None:
70
+ W_eff = W_eff * self.mask # elementwise mask
71
+
72
+ recon_x = z @ W_eff.T + self.bias
73
+ return recon_x, self.log_var
74
+
75
+ # Regularizers
76
+ def group_sparsity_penalty(self, l1_strength: float = 1e-3):
77
+ """
78
+ Group sparsity across genes per latent factor.
79
+ """
80
+ W_eff = self.W * self.mask if self.mask is not None else self.W
81
+ return l1_strength * torch.sum(torch.abs(W_eff))
82
+
83
+ def laplacian_penalty(self, L: torch.Tensor, lap_strength: float = 1e-3):
84
+ """
85
+ Laplacian smoothness: tr(W^T L W).
86
+ """
87
+ W_eff = self.W * self.mask if self.mask is not None else self.W
88
+ LW = torch.sparse.mm(L, W_eff) if L.is_sparse else torch.matmul(L, W_eff)
89
+ penalty = torch.sum(W_eff * LW) # trace approx
90
+ return lap_strength * penalty
@@ -0,0 +1,71 @@
1
+ """
2
+ Encoder for StructuredFactorVAE
3
+ """
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+
9
+ class StructuredEncoder(nn.Module):
10
+ """
11
+ Encoder for StructuredFactorVAE.
12
+ Maps normalized gene expression (log-CPM/TPM) to latent Gaussian parameters.
13
+
14
+ Parameters
15
+ ----------
16
+ n_genes : int
17
+ Number of input genes (features).
18
+ n_latent : int
19
+ Dimensionality of latent space (number of biological modules).
20
+ hidden_dims : list of int, optional
21
+ Sizes of hidden layers. Default: [512, 256, 128].
22
+ dropout : float
23
+ Dropout probability applied after hidden layers.
24
+ """
25
+ def __init__(self, n_genes: int, n_latent: int,
26
+ hidden_dims: list[int] = None,
27
+ dropout: float = 0.1):
28
+ super().__init__()
29
+ self.n_genes = n_genes
30
+ self.n_latent = n_latent
31
+ self.hidden_dims = hidden_dims if hidden_dims is not None else [512, 256, 128]
32
+ self.dropout = dropout
33
+
34
+ # Build feedforward encoder network
35
+ modules = []
36
+ input_dim = n_genes
37
+ for h_dim in self.hidden_dims:
38
+ modules.append(nn.Linear(input_dim, h_dim))
39
+ modules.append(nn.ReLU())
40
+ if dropout > 0:
41
+ modules.append(nn.Dropout(dropout))
42
+ input_dim = h_dim
43
+ self.encoder = nn.Sequential(*modules)
44
+
45
+ # Outputs: mean and log-variance
46
+ self.fc_mu = nn.Linear(input_dim, n_latent)
47
+ self.fc_logvar = nn.Linear(input_dim, n_latent)
48
+
49
+ def forward(self, x: torch.Tensor):
50
+ """
51
+ Forward pass through encoder.
52
+
53
+ Parameters
54
+ ----------
55
+ x : torch.Tensor
56
+ Input expression data (batch, G).
57
+
58
+ Returns
59
+ -------
60
+ mu : torch.Tensor
61
+ Latent mean (batch, K).
62
+ logvar : torch.Tensor
63
+ Latent log variance (batch, K).
64
+ z : torch.Tensor
65
+ Sampled latent code (batch, K).
66
+ """
67
+ h = self.encoder(x)
68
+ mu = self.fc_mu(h)
69
+ logvar = self.fc_logvar(h)
70
+
71
+ return mu, logvar
bsvae/models/losses.py ADDED
@@ -0,0 +1,116 @@
1
+ """
2
+ Losses for StructuredFactorVAE.
3
+ """
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ from typing import Optional, Dict
8
+
9
+ def gaussian_nll(x, recon_x, log_var=None, reduction="mean"):
10
+ """
11
+ Gaussian negative log-likelihood per gene.
12
+ """
13
+ if log_var is None:
14
+ return F.mse_loss(recon_x, x, reduction=reduction)
15
+
16
+ var = torch.exp(log_var)
17
+ nll = 0.5 * (torch.log(2 * torch.pi * var) + (x - recon_x) ** 2 / var)
18
+ if reduction == "mean":
19
+ return nll.mean()
20
+ elif reduction == "sum":
21
+ return nll.sum()
22
+ else:
23
+ return nll
24
+
25
+
26
+ def kl_normal_loss(mu, logvar, reduction="sum"):
27
+ """
28
+ KL divergence between q(z|x)=N(mu, sigma^2) and prior p(z)=N(0,I).
29
+ """
30
+ kl = -0.5 * (1 + logvar - mu.pow(2) - logvar.exp())
31
+ if reduction == "sum":
32
+ return kl.sum()
33
+ elif reduction == "mean":
34
+ return kl.mean()
35
+ else:
36
+ return kl
37
+
38
+
39
+ class BaseLoss(nn.Module):
40
+ """
41
+ Base loss for StructuredFactorVAE with biological regularizers.
42
+ """
43
+ def __init__(self, beta: float = 1.0,
44
+ l1_strength: float = 1e-3,
45
+ lap_strength: float = 1e-4,
46
+ record_loss_every: int = 50):
47
+ super().__init__()
48
+ self.beta = beta
49
+ self.l1_strength = l1_strength
50
+ self.lap_strength = lap_strength
51
+ self.n_train_steps = 0
52
+ self.record_loss_every = record_loss_every
53
+
54
+ def forward(self,
55
+ x: torch.Tensor, recon_x: torch.Tensor,
56
+ mu: torch.Tensor, logvar: torch.Tensor,
57
+ model: nn.Module,
58
+ L: Optional[torch.Tensor] = None,
59
+ storer: Optional[Dict[str, list]] = None,
60
+ is_train: bool = True) -> torch.Tensor:
61
+ """
62
+ Compute StructuredFactorVAE loss.
63
+
64
+ Parameters
65
+ ----------
66
+ x : torch.Tensor
67
+ Input data (batch, G).
68
+ recon_x : torch.Tensor
69
+ Reconstruction from decoder (batch, G).
70
+ mu : torch.Tensor
71
+ Latent mean (batch, K).
72
+ logvar : torch.Tensor
73
+ Latent log variance (batch, K).
74
+ model : nn.Module
75
+ Structured decoder (with sparsity/laplacian methods).
76
+ storer : dict
77
+ Dictionary for logging intermediate values.
78
+ is_train : bool
79
+ Whether in training mode.
80
+
81
+ Returns
82
+ -------
83
+ loss : torch.Tensor
84
+ Total loss for this batch.
85
+ """
86
+ # Reconstruction loss: Gaussian NLL if available, else MSE
87
+ if hasattr(model.decoder, "log_var") and model.decoder.log_var is not None:
88
+ recon_loss = gaussian_nll(x, recon_x, log_var=model.decoder.log_var,
89
+ reduction="mean")
90
+ else:
91
+ recon_loss = F.mse_loss(recon_x, x, reduction="mean")
92
+
93
+ # KL divergence
94
+ kl_loss = kl_normal_loss(mu, logvar, reduction="mean")
95
+
96
+ # Biological regularizers
97
+ sparsity_loss = model.decoder.group_sparsity_penalty(self.l1_strength)
98
+ laplacian_loss = 0.0
99
+ if model.laplacian_matrix is not None:
100
+ laplacian_loss = model.decoder.laplacian_penalty(
101
+ model.laplacian_matrix, self.lap_strength
102
+ )
103
+
104
+ # Total loss
105
+ loss = recon_loss + self.beta * kl_loss + sparsity_loss + laplacian_loss
106
+
107
+ # Logging
108
+ if storer is not None:
109
+ storer.setdefault("recon_loss", []).append(recon_loss.item())
110
+ storer.setdefault("kl_loss", []).append(kl_loss.item())
111
+ storer.setdefault("sparsity_loss", []).append(sparsity_loss.detach().item())
112
+ if model.laplacian_matrix is not None:
113
+ storer.setdefault("laplacian_loss", []).append(laplacian_loss.detach().item())
114
+ storer.setdefault("loss", []).append(loss.item())
115
+
116
+ return loss
@@ -0,0 +1,75 @@
1
+ """
2
+ Structured Factor VAE: wraps encoder + decoder for end-to-end training.
3
+ """
4
+ import torch
5
+ import torch.nn as nn
6
+ from .vae import BaseVAE
7
+ from .encoder import StructuredEncoder
8
+ from .decoder import StructuredDecoder
9
+ from ..utils.initialization import weights_init
10
+
11
+ class StructuredFactorVAE(BaseVAE):
12
+ """
13
+ Structured Factor VAE for gene expression.
14
+
15
+ Encoder: maps log-CPM input (batch, G) → latent mean + logvar.
16
+ Decoder: maps latent z → reconstructed expression (batch, G).
17
+ Supports optional gene-specific variance (log_var).
18
+
19
+ Parameters
20
+ ----------
21
+ n_genes : int
22
+ Number of input genes.
23
+ n_latent : int
24
+ Number of latent dimensions (modules).
25
+ hidden_dims : list of int
26
+ Hidden layer sizes for encoder.
27
+ dropout : float
28
+ Dropout probability in encoder.
29
+ mask : torch.Tensor or None
30
+ Optional binary gene×module mask for decoder (G, K).
31
+ init_sd : float
32
+ Std for decoder weight initialization.
33
+ learn_var : bool
34
+ If True, decoder learns per-gene log variance.
35
+ """
36
+ def __init__(self, n_genes: int, n_latent: int,
37
+ hidden_dims=None, dropout: float = 0.1,
38
+ mask: torch.Tensor = None,
39
+ init_sd: float = 0.02,
40
+ learn_var: bool = False,
41
+ L: torch.Tensor = None):
42
+ super().__init__(n_genes, n_latent)
43
+ self.hidden_dims = hidden_dims or [512, 256, 128]
44
+ self.dropout = dropout
45
+ self.encoder = StructuredEncoder(
46
+ n_genes=n_genes,
47
+ n_latent=n_latent,
48
+ hidden_dims=hidden_dims,
49
+ dropout=dropout
50
+ )
51
+ self.decoder = StructuredDecoder(
52
+ n_genes=n_genes,
53
+ n_latent=n_latent,
54
+ mask=mask,
55
+ init_sd=init_sd,
56
+ learn_var=learn_var
57
+ )
58
+ self.laplacian_matrix = L
59
+
60
+ def forward(self, x: torch.Tensor):
61
+ mu, logvar = self.encoder(x)
62
+ z = self.reparameterize(mu, logvar)
63
+ recon_x, log_var = self.decoder(z)
64
+ return recon_x, mu, logvar, z, log_var
65
+
66
+ def group_sparsity_penalty(self, l1_strength: float = 1e-3):
67
+ return self.decoder.group_sparsity_penalty(l1_strength)
68
+
69
+ def laplacian_penalty(self, L: torch.Tensor, lap_strength: float = 1e-3):
70
+ return self.decoder.laplacian_penalty(L, lap_strength)
71
+
72
+ def reset_parameters(self, activation: str = "relu"):
73
+ """Reset all learnable parameters with custom init."""
74
+ self.activation = activation
75
+ self.apply(lambda m: weights_init(m, activation=self.activation))