PathwayVI 0.1.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.
PathwayVI/PathwayVI.py ADDED
@@ -0,0 +1,1310 @@
1
+ import pyro
2
+ import pyro.distributions as dist
3
+ from pyro.optim import ExponentialLR
4
+ from pyro.infer import SVI, JitTraceEnum_ELBO, TraceEnum_ELBO, config_enumerate
5
+
6
+ import torch
7
+ import torch.nn as nn
8
+ from torch.utils.data import DataLoader
9
+ from torch.distributions.utils import logits_to_probs, probs_to_logits, clamp_probs
10
+ from torch.distributions import constraints
11
+ from torch.distributions.transforms import SoftmaxTransform
12
+ from torch.func import jacrev, vmap
13
+
14
+ from .utils.custom_mlp import MLP, Exp, PathwayDecoder
15
+ from .utils.utils import CustomDataset, tensor_to_numpy, convert_to_tensor
16
+
17
+
18
+ from pyro.distributions import LKJCorrCholesky
19
+
20
+ import zuko
21
+ from pyro.contrib.zuko import ZukoToPyro
22
+
23
+ import os
24
+ import argparse
25
+ import random
26
+ import numpy as np
27
+ import datatable as dt
28
+ from tqdm import tqdm
29
+ from scipy import sparse
30
+ from sklearn.cluster import KMeans
31
+
32
+ import scanpy as sc
33
+ from .atac import binarize
34
+
35
+ from typing import Literal
36
+
37
+ import warnings
38
+ warnings.filterwarnings("ignore")
39
+
40
+ import dill as pickle
41
+ import gzip
42
+ from packaging.version import Version
43
+ torch_version = torch.__version__
44
+
45
+
46
+ def set_random_seed(seed):
47
+ # Set seed for PyTorch
48
+ torch.manual_seed(seed)
49
+
50
+ # If using CUDA, set the seed for CUDA
51
+ if torch.cuda.is_available():
52
+ torch.cuda.manual_seed(seed)
53
+ torch.cuda.manual_seed_all(seed) # For multi-GPU setups.
54
+
55
+ # Set seed for NumPy
56
+ np.random.seed(seed)
57
+
58
+ # Set seed for Python's random module
59
+ random.seed(seed)
60
+
61
+ # Set seed for Pyro
62
+ pyro.set_rng_seed(seed)
63
+
64
+
65
+ class PathwayVI(nn.Module):
66
+ """SUccinct REpresentation of single-omics cells
67
+
68
+ Parameters
69
+ ----------
70
+ inpute_size
71
+ Number of features (e.g., genes, peaks, proteins, etc.) per cell.
72
+ codebook_size
73
+ Number of metacells.
74
+ context_sizes
75
+ Number of context factors.
76
+ transforms
77
+ Number of neural spline flows
78
+ z_dim
79
+ Dimensionality of latent states and metacells.
80
+ hidden_layers
81
+ A list gives the numbers of neurons for each hidden layer.
82
+ flow_hidden_layers
83
+ A list gives the numbers of neurons for each hidden layer in neural spline flows.
84
+
85
+ One of the following:
86
+ * ``'negbinomial'`` - negative binomial distribution (default)
87
+ * ``'poisson'`` - poisson distribution
88
+ * ``'multinomial'`` - multinomial distribution
89
+ use_cuda
90
+ A boolean option for switching on cuda device.
91
+
92
+ Examples
93
+ --------
94
+ >>>
95
+ >>>
96
+ >>>
97
+
98
+ """
99
+ def __init__(self,
100
+ input_dim: int,
101
+ n_pathways: int,
102
+ masks: torch.Tensor,
103
+ n_latent: int = 20,
104
+ codebook_size: int = 50,
105
+ context_sizes: list = [0],
106
+ perturb_size: int = 0,
107
+ covariate_sizes: list = [0],
108
+ total_count: int = 10000,
109
+ dispersion: float = 10.0,
110
+ hidden_layers: list = [500],
111
+ hidden_layer_activation: Literal['relu','softplus','leakyrelu','linear'] = 'relu',
112
+ flow_transforms: int = 5,
113
+ flow_hidden_layers: list = [256,256],
114
+ nn_dropout: float = 0.1,
115
+ post_layer_fct: list = ['layernorm'],
116
+ post_act_fct: list = None,
117
+ config_enum: str = 'parallel',
118
+ use_cuda: bool = True,
119
+ seed: int = 42,
120
+ dtype = torch.float32, # type: ignore
121
+ ):
122
+ super().__init__()
123
+
124
+ self.input_dim = input_dim
125
+ self.n_pathways = n_pathways
126
+ self.n_latent = n_latent
127
+ self.codebook_size = codebook_size
128
+ self.context_sizes = context_sizes
129
+ self.perturb_size = perturb_size
130
+ self.covariate_sizes = covariate_sizes
131
+ self.covariate_dim = n_pathways + n_latent #if covariate_dim is None else covariate_dim
132
+ self.dispersion = dispersion
133
+ self.latent_dim = n_pathways + n_latent
134
+ self.hidden_layers = hidden_layers
135
+ self.decoder_hidden_layers = hidden_layers[::-1]
136
+ self.allow_broadcast = config_enum == 'parallel'
137
+ self.use_cuda = use_cuda
138
+ self.options = None
139
+ self.dtype = dtype
140
+ self.nn_dropout = nn_dropout
141
+ self.post_layer_fct = post_layer_fct
142
+ self.post_act_fct = post_act_fct
143
+ self.hidden_layer_activation = hidden_layer_activation
144
+ self.total_count = int(total_count)
145
+
146
+ self.codebook_weights = None
147
+ self.centroids = None
148
+
149
+ self.masks = masks
150
+
151
+ self.transforms = flow_transforms
152
+ self.flow_hidden_layers = flow_hidden_layers
153
+
154
+ self.seed = seed
155
+
156
+ self.lkj_eta = 5.0
157
+ self.sparse_scale = 1.0
158
+
159
+ set_random_seed(seed)
160
+ self.setup_networks()
161
+
162
+ def setup_networks(self):
163
+ latent_dim = self.latent_dim
164
+ hidden_sizes = self.hidden_layers
165
+
166
+ nn_layer_norm, nn_batch_norm, nn_layer_dropout = False, False, False
167
+ na_layer_norm, na_batch_norm, na_layer_dropout = False, False, False
168
+
169
+ if self.post_layer_fct is not None:
170
+ nn_layer_norm=True if ('layernorm' in self.post_layer_fct) or ('layer_norm' in self.post_layer_fct) else False
171
+ nn_batch_norm=True if ('batchnorm' in self.post_layer_fct) or ('batch_norm' in self.post_layer_fct) else False
172
+ nn_layer_dropout=True if 'dropout' in self.post_layer_fct else False
173
+
174
+ if self.post_act_fct is not None:
175
+ na_layer_norm=True if ('layernorm' in self.post_act_fct) or ('layer_norm' in self.post_act_fct) else False
176
+ na_batch_norm=True if ('batchnorm' in self.post_act_fct) or ('batch_norm' in self.post_act_fct) else False
177
+ na_layer_dropout=True if 'dropout' in self.post_act_fct else False
178
+
179
+ if nn_layer_norm and nn_batch_norm and nn_layer_dropout:
180
+ post_layer_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.Dropout(self.nn_dropout),nn.BatchNorm1d(layer.module.out_features), nn.LayerNorm(layer.module.out_features))
181
+ elif nn_layer_norm and nn_layer_dropout:
182
+ post_layer_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.Dropout(self.nn_dropout), nn.LayerNorm(layer.module.out_features))
183
+ elif nn_batch_norm and nn_layer_dropout:
184
+ post_layer_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.Dropout(self.nn_dropout), nn.BatchNorm1d(layer.module.out_features))
185
+ elif nn_layer_norm and nn_batch_norm:
186
+ post_layer_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.BatchNorm1d(layer.module.out_features), nn.LayerNorm(layer.module.out_features))
187
+ elif nn_layer_norm:
188
+ post_layer_fct = lambda layer_ix, total_layers, layer: nn.LayerNorm(layer.module.out_features)
189
+ elif nn_batch_norm:
190
+ post_layer_fct = lambda layer_ix, total_layers, layer:nn.BatchNorm1d(layer.module.out_features)
191
+ elif nn_layer_dropout:
192
+ post_layer_fct = lambda layer_ix, total_layers, layer: nn.Dropout(self.nn_dropout)
193
+ else:
194
+ post_layer_fct = lambda layer_ix, total_layers, layer: None
195
+
196
+ if na_layer_norm and na_batch_norm and na_layer_dropout:
197
+ post_act_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.Dropout(self.nn_dropout),nn.BatchNorm1d(layer.module.out_features), nn.LayerNorm(layer.module.out_features))
198
+ elif na_layer_norm and na_layer_dropout:
199
+ post_act_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.Dropout(self.nn_dropout), nn.LayerNorm(layer.module.out_features))
200
+ elif na_batch_norm and na_layer_dropout:
201
+ post_act_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.Dropout(self.nn_dropout), nn.BatchNorm1d(layer.module.out_features))
202
+ elif na_layer_norm and na_batch_norm:
203
+ post_act_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.BatchNorm1d(layer.module.out_features), nn.LayerNorm(layer.module.out_features))
204
+ elif na_layer_norm:
205
+ post_act_fct = lambda layer_ix, total_layers, layer: nn.LayerNorm(layer.module.out_features)
206
+ elif na_batch_norm:
207
+ post_act_fct = lambda layer_ix, total_layers, layer:nn.BatchNorm1d(layer.module.out_features)
208
+ elif na_layer_dropout:
209
+ post_act_fct = lambda layer_ix, total_layers, layer: nn.Dropout(self.nn_dropout)
210
+ else:
211
+ post_act_fct = lambda layer_ix, total_layers, layer: None
212
+
213
+ if self.hidden_layer_activation == 'relu':
214
+ activate_fct = nn.ReLU
215
+ elif self.hidden_layer_activation == 'softplus':
216
+ activate_fct = nn.Softplus
217
+ elif self.hidden_layer_activation == 'leakyrelu':
218
+ activate_fct = nn.LeakyReLU
219
+ elif self.hidden_layer_activation == 'linear':
220
+ activate_fct = nn.Identity
221
+
222
+ self.encoder_alpha = MLP(
223
+ [self.latent_dim] + hidden_sizes + [self.codebook_size],
224
+ activation=activate_fct,
225
+ output_activation=None,
226
+ post_layer_fct=post_layer_fct,
227
+ post_act_fct=post_act_fct,
228
+ allow_broadcast=self.allow_broadcast,
229
+ use_cuda=self.use_cuda,
230
+ )
231
+
232
+ self.encoder_coord = MLP(
233
+ [self.input_dim] + hidden_sizes + [[latent_dim, latent_dim]],
234
+ activation=activate_fct,
235
+ output_activation=[None, Exp],
236
+ post_layer_fct=post_layer_fct,
237
+ post_act_fct=post_act_fct,
238
+ allow_broadcast=self.allow_broadcast,
239
+ use_cuda=self.use_cuda,
240
+ )
241
+
242
+ self.latent_decoder = NormalizeFlow(z_dim=self.latent_dim, num_flows=self.transforms, flow_type='planar')
243
+ if self.use_cuda:
244
+ self.latent_decoder.to('cuda')
245
+
246
+ if np.sum(self.context_sizes)>0:
247
+ self.context_tokens = nn.ModuleList()
248
+ for context_size in self.context_sizes:
249
+ self.context_tokens.append(MLP(
250
+ [context_size] + self.decoder_hidden_layers + [self.latent_dim],
251
+ activation=activate_fct,
252
+ output_activation=None,
253
+ post_layer_fct=post_layer_fct,
254
+ post_act_fct=post_act_fct,
255
+ allow_broadcast=self.allow_broadcast,
256
+ use_cuda=self.use_cuda,
257
+ )
258
+ )
259
+ self.context_effect = MLP(
260
+ [self.latent_dim+self.latent_dim] + self.decoder_hidden_layers + [self.latent_dim],
261
+ activation=activate_fct,
262
+ output_activation=None,
263
+ post_layer_fct=post_layer_fct,
264
+ post_act_fct=post_act_fct,
265
+ allow_broadcast=self.allow_broadcast,
266
+ use_cuda=self.use_cuda,
267
+ )
268
+
269
+ if self.perturb_size>0:
270
+ self.perturb_token = MLP(
271
+ [self.perturb_size] + self.decoder_hidden_layers + [self.latent_dim],
272
+ activation=activate_fct,
273
+ output_activation=None,
274
+ post_layer_fct=post_layer_fct,
275
+ post_act_fct=post_act_fct,
276
+ allow_broadcast=self.allow_broadcast,
277
+ use_cuda=self.use_cuda,
278
+ )
279
+ self.perturb_effect = MLP(
280
+ [self.latent_dim+self.latent_dim] + self.decoder_hidden_layers + [self.latent_dim],
281
+ activation=activate_fct,
282
+ output_activation=None,
283
+ post_layer_fct=post_layer_fct,
284
+ post_act_fct=post_act_fct,
285
+ allow_broadcast=self.allow_broadcast,
286
+ use_cuda=self.use_cuda,
287
+ )
288
+
289
+ if np.sum(self.covariate_sizes)>0:
290
+ self.covariate_tokens = nn.ModuleList()
291
+ for covariate_size in self.covariate_sizes:
292
+ self.covariate_tokens.append(MLP(
293
+ [covariate_size] + self.decoder_hidden_layers + [self.covariate_dim],
294
+ activation=activate_fct,
295
+ output_activation=None,
296
+ post_layer_fct=post_layer_fct,
297
+ post_act_fct=post_act_fct,
298
+ allow_broadcast=self.allow_broadcast,
299
+ use_cuda=self.use_cuda,
300
+ )
301
+ )
302
+ self.covariate_effect = MLP(
303
+ [self.covariate_dim] + self.decoder_hidden_layers + [self.latent_dim],
304
+ activation=activate_fct,
305
+ output_activation=None,
306
+ post_layer_fct=post_layer_fct,
307
+ post_act_fct=post_act_fct,
308
+ allow_broadcast=self.allow_broadcast,
309
+ use_cuda=self.use_cuda,
310
+ )
311
+
312
+ self.decoder_log_mu = nn.ModuleList()
313
+ for _ in range(self.n_latent):
314
+ self.decoder_log_mu.append(MLP(
315
+ [1] + hidden_sizes + [self.input_dim],
316
+ activation=nn.Identity, # linear
317
+ output_activation=None,
318
+ allow_broadcast=self.allow_broadcast,
319
+ use_cuda=self.use_cuda,
320
+ ))
321
+
322
+ self.pathway_decoder_log_mu = PathwayDecoder(n_latent=self.n_pathways,
323
+ n_genes=self.input_dim,
324
+ n_pathways=self.n_pathways,
325
+ masks=self.masks,
326
+ sub_hidden=hidden_sizes,
327
+ activation=activate_fct,
328
+ output_activation=None,
329
+ post_layer_fct=post_layer_fct,
330
+ post_act_fct=post_act_fct,
331
+ allow_broadcast=self.allow_broadcast,
332
+ use_cuda=self.use_cuda)
333
+
334
+ self.codebook = MLP(
335
+ [self.codebook_size] + hidden_sizes + [latent_dim],
336
+ activation=activate_fct,
337
+ output_activation=None,
338
+ post_layer_fct=post_layer_fct,
339
+ post_act_fct=post_act_fct,
340
+ allow_broadcast=self.allow_broadcast,
341
+ use_cuda=self.use_cuda,
342
+ )
343
+
344
+ if self.use_cuda:
345
+ self.cuda()
346
+
347
+ def get_device(self):
348
+ return next(self.parameters()).device
349
+
350
+ def lse_pool(self, split_logits: torch.Tensor) -> torch.Tensor:
351
+ """LSE pooling over the pathway split dimension."""
352
+ return torch.logsumexp(split_logits, dim=1)
353
+
354
+ def cutoff(self, xs, thresh=None):
355
+ eps = torch.finfo(xs.dtype).eps
356
+
357
+ if not thresh is None:
358
+ if eps < thresh:
359
+ eps = thresh
360
+
361
+ xs = xs.clamp(min=eps)
362
+
363
+ if torch.any(torch.isnan(xs)):
364
+ xs[torch.isnan(xs)] = eps
365
+
366
+ return xs
367
+
368
+ def softmax(self, xs):
369
+ #xs = SoftmaxTransform()(xs)
370
+ #xs = dist.Multinomial(total_count=1, logits=xs).mean
371
+ logits = xs - xs.logsumexp(-1, keepdim=True) # log softmax
372
+ return logits.exp()
373
+
374
+ def sigmoid(self, xs):
375
+ #sigm_enc = nn.Sigmoid()
376
+ #xs = sigm_enc(xs)
377
+ #xs = clamp_probs(xs)
378
+ xs = dist.Bernoulli(logits=xs).mean
379
+ return xs
380
+
381
+ def softmax_logit(self, xs):
382
+ eps = torch.finfo(xs.dtype).eps
383
+ xs = self.softmax(xs)
384
+ xs = torch.logit(xs, eps=eps)
385
+ return xs
386
+
387
+ def logit(self, xs):
388
+ eps = torch.finfo(xs.dtype).eps
389
+ xs = torch.logit(xs, eps=eps)
390
+ return xs
391
+
392
+ def dirimulti_param(self, xs):
393
+ xs = self.dirimulti_mass * self.sigmoid(xs)
394
+ return xs
395
+
396
+ def multi_param(self, xs):
397
+ xs = self.softmax(xs)
398
+ return xs
399
+
400
+ def model(self, xs, cs=None, ps=None, fs=None):
401
+ pyro.module('sure', self)
402
+
403
+ eps = torch.finfo(xs.dtype).eps
404
+ batch_size = xs.size(0)
405
+ self.options = dict(dtype=xs.dtype, device=xs.device)
406
+
407
+ # latent modeling and data likelihood
408
+ acs_scale = pyro.param("codebook_scale", xs.new_ones(self.latent_dim), constraint=constraints.positive)
409
+
410
+ I = torch.eye(self.codebook_size)
411
+ acs_loc = self.codebook(I)
412
+
413
+ with pyro.plate('data'):
414
+ prior = torch.zeros(batch_size, self.codebook_size, **self.options)
415
+ ns = pyro.sample('n', dist.OneHotCategorical(logits=prior))
416
+ _, ind = torch.topk(ns, 1)
417
+
418
+ zn_loc = acs_loc[ind.squeeze()]
419
+ zn_scale = acs_scale
420
+ zns = pyro.sample('coord', dist.Normal(zn_loc, zn_scale).to_event(1))
421
+
422
+ zs_ = torch.zeros_like(zns)
423
+ zs_ += zns
424
+ if (np.sum(self.context_sizes)>0) and (cs is not None):
425
+ zct = torch.zeros_like(zs_)
426
+ shift = 0
427
+ for i, context_size in enumerate(self.context_sizes):
428
+ cs_i = cs[:,shift:(shift+context_size)]
429
+ zct += self.context_tokens[i](cs_i)
430
+ shift += context_size
431
+ zcs = self.context_effect([zs_, zct])
432
+ zs_ += zcs
433
+
434
+ if (self.perturb_size>0) and (ps is not None):
435
+ zpt = self.perturb_token(ps)
436
+ zps = self.perturb_effect([zs_, zpt])
437
+ zs_ += zps
438
+
439
+ if (np.sum(self.covariate_sizes)>0) and (fs is not None):
440
+ zft = torch.zeros(batch_size, self.covariate_dim).to(self.get_device())
441
+ shift = 0
442
+ for i, covariate_size in enumerate(self.covariate_sizes):
443
+ fs_i = fs[:,shift:(shift+covariate_size)]
444
+ zft += self.covariate_tokens[i](fs_i)
445
+ shift += covariate_size
446
+ zfs = self.covariate_effect(zft)
447
+ else:
448
+ zfs = torch.zeros(batch_size, self.latent_dim).to(self.get_device())
449
+
450
+ zs = self.latent_decoder(zs_)
451
+ zs += zfs
452
+
453
+ pathway_chunk,novel_chunk = torch.chunk(zs, chunks=[self.n_pathways,self.n_latent], dim=1)
454
+
455
+ chunks = torch.chunk(novel_chunk, chunks=len(self.decoder_log_mu), dim=1)
456
+ log_mu_parts = [self.decoder_log_mu[i](chunk) for i, chunk in enumerate(chunks)]
457
+ novel_log_mu = torch.stack(log_mu_parts, dim=1) # [N, E, H] (假设输出带专家维度)
458
+
459
+ pathway_log_mu = self.pathway_decoder_log_mu(pathway_chunk)
460
+
461
+ total_log_mu = torch.stack([pathway_log_mu, novel_log_mu], dim=1)
462
+ log_mu = self.lse_pool(total_log_mu)
463
+ logits = log_mu - log_mu.logsumexp(-1, keepdim=True) # log softmax
464
+ pyro.sample('x', dist.Multinomial(total_count=self.total_count, logits=logits), obs=xs)
465
+
466
+ def guide(self, xs, cs=None, ps=None, fs=None):
467
+ if self.options is None:
468
+ self.options = dict(dtype=xs.dtype, device=xs.device)
469
+
470
+ with pyro.plate('data'):
471
+ zn_loc, zn_scale = self.encoder_coord(xs)
472
+ zns = pyro.sample('coord', dist.Normal(zn_loc, zn_scale).to_event(1))
473
+
474
+ alpha = self.encoder_alpha(zns)
475
+ ns = pyro.sample('n', dist.OneHotCategorical(logits=alpha))
476
+
477
+ def log_prob(self, xs, zs):
478
+ mu = self._decode_latent(zs)
479
+ logp = dist.Multinomial(probs=mu).log_prob(xs).sum()
480
+ return logp
481
+
482
+ def _decode_latent(self, zs):
483
+ zs = self.latent_decoder(zs)
484
+
485
+ pathway_chunk,novel_chunk = torch.chunk(zs, chunks=[self.n_pathways,self.n_latent], dim=1)
486
+
487
+ chunks = torch.chunk(novel_chunk, chunks=len(self.decoder_log_mu), dim=1)
488
+ log_mu_parts = [self.decoder_log_mu[i](chunk) for i, chunk in enumerate(chunks)]
489
+ novel_log_mu = torch.stack(log_mu_parts, dim=1) # [N, E, H] (假设输出带专家维度)
490
+
491
+ pathway_log_mu = self.pathway_decoder_log_mu(pathway_chunk)
492
+
493
+ total_log_mu = torch.stack([pathway_log_mu,novel_log_mu], dim=1)
494
+ log_mu = self.lse_pool(total_log_mu)
495
+ logits = log_mu - log_mu.logsumexp(-1, keepdim=True) # log softmax
496
+ mu = logits.exp()
497
+
498
+ return mu
499
+
500
+ def manifold_representation(self, zs, batch_size: int = 1024, show_progress: bool = True):
501
+ """
502
+ """
503
+ ##################
504
+ zs = convert_to_tensor(zs, device='cpu')
505
+ dataset = CustomDataset(zs)
506
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
507
+
508
+ zs = []
509
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
510
+ for Z_batch, _ in dataloader:
511
+ Z_batch = Z_batch.to(self.get_device())
512
+ z = self.latent_decoder(Z_batch)
513
+ zs.append(tensor_to_numpy(z))
514
+ pbar.update(1)
515
+
516
+ zs = np.concatenate(zs)
517
+ return zs
518
+
519
+ def decode(self, zs, batch_size: int = 1024, show_progress: bool = True):
520
+ """
521
+ Return cells' latent representations
522
+
523
+ Parameters
524
+ ----------
525
+ xs
526
+ Single-cell expression matrix. It should be a Numpy array or a Pytorch Tensor.
527
+ batch_size
528
+ Size of batch processing.
529
+ show_progress
530
+ Verbose on or off
531
+ """
532
+ ##################
533
+ zs = convert_to_tensor(zs, device='cpu')
534
+ dataset = CustomDataset(zs)
535
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
536
+
537
+ mu = []
538
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
539
+ for Z_batch, _ in dataloader:
540
+ Z_batch = Z_batch.to(self.get_device())
541
+ mu_ = self._decode_latent(Z_batch)
542
+ mu.append(tensor_to_numpy(mu_))
543
+ pbar.update(1)
544
+
545
+ mu = np.concatenate(mu)
546
+ return mu
547
+
548
+ def _get_basal_embedding(self, xs):
549
+ zns, _ = self.encoder_coord(xs)
550
+ return zns
551
+
552
+ def get_cell_embedding(self,
553
+ xs,
554
+ css:list = None,
555
+ ps: np.array = None,
556
+ batch_size: int = 1024,
557
+ show_progress: bool = True):
558
+ """
559
+ Return cells' latent representations
560
+
561
+ Parameters
562
+ ----------
563
+ xs
564
+ Single-cell expression matrix. It should be a Numpy array or a Pytorch Tensor.
565
+ batch_size
566
+ Size of batch processing.
567
+ show_progress
568
+ Verbose on or off
569
+ """
570
+ ##################
571
+ xs = self.preprocess(xs)
572
+ xs = convert_to_tensor(xs, device='cpu')
573
+ dataset = CustomDataset(xs)
574
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
575
+
576
+ Z = []
577
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
578
+ for X_batch, _ in dataloader:
579
+ X_batch = X_batch.to(self.get_device())
580
+ zns = self._get_basal_embedding(X_batch)
581
+ Z.append(tensor_to_numpy(zns))
582
+ pbar.update(1)
583
+
584
+ Z = np.concatenate(Z)
585
+
586
+ #######################
587
+ CT = np.zeros_like(Z)
588
+ if css is not None:
589
+ for i in range(len(css)):
590
+ CT += self.get_context_token(css[i], i, batch_size=batch_size, show_progress=show_progress)
591
+ Z += self.get_context_effect(Z, CT, batch_size=batch_size, show_progress=show_progress)
592
+
593
+ #######################
594
+ PT = np.zeros_like(Z)
595
+ if ps is not None:
596
+ PT = self.get_perturb_token(ps, batch_size=batch_size, show_progress=show_progress)
597
+ Z += self.get_perturb_effect(Z, PT)
598
+
599
+ #####################
600
+ Z = convert_to_tensor(Z, device='cpu')
601
+ dataset = CustomDataset(Z)
602
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
603
+
604
+ latent = []
605
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
606
+ for ZC_batch, _ in dataloader:
607
+ ZC_batch = ZC_batch.to(self.get_device())
608
+ zns = self.latent_decoder(ZC_batch)
609
+ latent.append(tensor_to_numpy(zns))
610
+ pbar.update(1)
611
+
612
+ latent = np.concatenate(latent)
613
+ return latent
614
+
615
+ def get_context_token(self, cs, i, batch_size=1024, show_progress=True):
616
+ cs = convert_to_tensor(cs, dtype=self.dtype, device='cpu')
617
+
618
+ dataset = CustomDataset(cs)
619
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
620
+
621
+ A = []
622
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
623
+ for C_batch, idx in dataloader:
624
+ C_batch = C_batch.to(self.get_device())
625
+
626
+ dzs = self.context_tokens[i](C_batch)
627
+
628
+ A.append(tensor_to_numpy(dzs))
629
+ pbar.update(1)
630
+
631
+ A = np.concatenate(A)
632
+ return A
633
+
634
+ def get_context_effect(self, zs, zcs, batch_size=1024, show_progress=True):
635
+ zs = convert_to_tensor(zs, dtype=self.dtype, device='cpu')
636
+ zcs = convert_to_tensor(zcs, dtype=self.dtype, device='cpu')
637
+
638
+ dataset = CustomDataset(zcs)
639
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
640
+
641
+ A = []
642
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
643
+ for C_batch, idx in dataloader:
644
+ C_batch = C_batch.to(self.get_device())
645
+ Z_batch = zs[idx].to(self.get_device())
646
+
647
+ dzs = self.context_effect([Z_batch,C_batch])
648
+
649
+ A.append(tensor_to_numpy(dzs))
650
+ pbar.update(1)
651
+
652
+ A = np.concatenate(A)
653
+ return A
654
+
655
+ def get_perturb_token(self, ps, batch_size=1024, show_progress=True):
656
+ ps = convert_to_tensor(ps, dtype=self.dtype, device='cpu')
657
+
658
+ dataset = CustomDataset(ps)
659
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
660
+
661
+ A = []
662
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
663
+ for P_batch, idx in dataloader:
664
+ P_batch = P_batch.to(self.get_device())
665
+
666
+ dzs = self.perturb_token(P_batch)
667
+
668
+ A.append(tensor_to_numpy(dzs))
669
+ pbar.update(1)
670
+
671
+ A = np.concatenate(A)
672
+ return A
673
+
674
+ def get_perturb_effect(self, zs, zps, batch_size=1024, show_progress=True):
675
+ zs = convert_to_tensor(zs, dtype=self.dtype, device='cpu')
676
+ zps = convert_to_tensor(zps, dtype=self.dtype, device='cpu')
677
+
678
+ dataset = CustomDataset(zs)
679
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
680
+
681
+ A = []
682
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
683
+ for Z_batch, idx in dataloader:
684
+ Z_batch = Z_batch.to(self.get_device())
685
+ P_batch = zps[idx].to(self.get_device())
686
+
687
+ dzs = self.perturb_effect([Z_batch,P_batch])
688
+
689
+ A.append(tensor_to_numpy(dzs))
690
+ pbar.update(1)
691
+
692
+ A = np.concatenate(A)
693
+ return A
694
+
695
+ def _codebook(self):
696
+ I = torch.eye(self.codebook_size)
697
+ acs_loc = self.codebook(I)
698
+ return acs_loc
699
+
700
+ def get_codebook(self):
701
+ return tensor_to_numpy(self._codebook())
702
+
703
+ def _code(self, xs):
704
+ #zns,_ = self.encoder_zn(xs)
705
+ zns = self._get_basal_embedding(xs)
706
+ alpha = self.encoder_alpha(zns)
707
+ return alpha
708
+
709
+ def _hard_assignments(self, xs):
710
+ alpha = self._code(xs)
711
+ res, ind = torch.topk(alpha, 1)
712
+ ns = torch.zeros_like(alpha).scatter_(1, ind, 1.0)
713
+ return ns,ind
714
+
715
+ def hard_assignments(self, xs, batch_size=1024, show_progress=True):
716
+ """
717
+ Map cells to metacells and return the assigned metacell identities.
718
+ """
719
+ xs = self.preprocess(xs)
720
+ xs = convert_to_tensor(xs, device='cpu')
721
+ dataset = CustomDataset(xs)
722
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
723
+
724
+ A = []
725
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
726
+ for X_batch, _ in dataloader:
727
+ X_batch = X_batch.to(self.get_device())
728
+ a,_ = self._hard_assignments(X_batch)
729
+ A.append(tensor_to_numpy(a))
730
+ pbar.update(1)
731
+
732
+ A = np.concatenate(A)
733
+ return A
734
+
735
+ def _soft_assignments(self, xs):
736
+ alpha = self._code(xs)
737
+ alpha = self.softmax(alpha)
738
+ return alpha
739
+
740
+ def soft_assignments(self, xs, batch_size=1024, show_progress=True):
741
+ """
742
+ Map cells to metacells and return the probabilistic values of metacell assignments
743
+ """
744
+ xs = self.preprocess(xs)
745
+ xs = convert_to_tensor(xs, device='cpu')
746
+ dataset = CustomDataset(xs)
747
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
748
+
749
+ A = []
750
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
751
+ for X_batch, _ in dataloader:
752
+ X_batch = X_batch.to(self.get_device())
753
+ a = self._soft_assignments(X_batch)
754
+ A.append(tensor_to_numpy(a))
755
+ pbar.update(1)
756
+
757
+ A = np.concatenate(A)
758
+ return A
759
+
760
+ def module_expression(self):
761
+ """
762
+ Generate gene expression prediction from given cell data and covariates.
763
+ This function can be used for simulating cells' transcription profiles at new conditions.
764
+
765
+ :param self: SURE model
766
+ :param xs: Cell data at the source condition
767
+ :param cs: Covariates specifying the target condition for generation
768
+ :param batch_size: Data size per batch
769
+ :param show_progress: Toggle on or off message output
770
+ """
771
+ A = []
772
+
773
+ z = np.array([[1]])
774
+ z = convert_to_tensor(z, device=self.get_device())
775
+
776
+ for i in range(self.latent_dim):
777
+ log_mu = self.decoder_log_mu[i](z)
778
+ #logits = log_mu - log_mu.logsumexp(-1, keepdim=True)
779
+ #mu = logits.exp()
780
+ A.append(tensor_to_numpy(log_mu).squeeze())
781
+
782
+ return np.vstack(A)
783
+
784
+ def get_full_representation(self, xs:np.array, cs_list:list=None, ps:np.array=None, batch_size=1024, show_progress=True):
785
+ """
786
+ """
787
+ xs = self.preprocess(xs)
788
+ xs = convert_to_tensor(xs, dtype=self.dtype, device='cpu')
789
+ if cs_list is not None:
790
+ cs = np.hstack(cs_list)
791
+ cs = convert_to_tensor(cs, dtype=self.dtype, device='cpu')
792
+ if ps is not None:
793
+ ps = convert_to_tensor(ps, dtype=self.dtype, device='cpu')
794
+
795
+ dataset = CustomDataset(xs)
796
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
797
+
798
+ A = []
799
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
800
+ for X_batch, idx in dataloader:
801
+ X_batch = X_batch.to(self.get_device())
802
+
803
+ z_basal = self._get_basal_embedding(X_batch)
804
+
805
+ zct = torch.zeros_like(z_basal)
806
+ if cs_list is not None:
807
+ C_batch = cs[idx].to(self.get_device())
808
+ shift = 0
809
+ for i, context_size in enumerate(self.context_sizes):
810
+ C_batch_i = C_batch[:, shift:(shift+context_size)]
811
+ zct += self.context_tokens[i](C_batch_i)
812
+ shift += context_size
813
+ zcs = self.context_effect([z_basal,zct])
814
+ z_basal += zcs
815
+
816
+ if ps is not None:
817
+ P_batch = ps[idx].to(self.get_device())
818
+ zpt = self.perturb_token(P_batch)
819
+ zps = self.perturb_effect([z_basal,zpt])
820
+ z_basal += zps
821
+
822
+ A.append(tensor_to_numpy(z_basal))
823
+ pbar.update(1)
824
+
825
+ A = np.concatenate(A)
826
+ return A
827
+
828
+ def predict(self, xs:np.array, cs_list:list=None, ps:np.array=None, fs_list:list=None, codebook:bool=False, batch_size=1024, show_progress=True):
829
+ """
830
+ Generate gene expression prediction from given cell data and covariates.
831
+ This function can be used for simulating cells' transcription profiles at new conditions.
832
+
833
+ :param self: DRVI model
834
+ :param xs: Cell data at the source condition
835
+ :param cs: Covariates specifying the target condition for generation
836
+ :param batch_size: Data size per batch
837
+ :param show_progress: Toggle on or off message output
838
+ """
839
+ xs = self.preprocess(xs)
840
+ xs = convert_to_tensor(xs, dtype=self.dtype, device='cpu')
841
+ if cs_list is not None:
842
+ cs = np.hstack(cs_list)
843
+ cs = convert_to_tensor(cs, dtype=self.dtype, device='cpu')
844
+ if ps is not None:
845
+ ps = convert_to_tensor(ps, dtype=self.dtype, device='cpu')
846
+ if fs_list is not None:
847
+ fs = np.hstack(fs_list)
848
+ fs = convert_to_tensor(fs, dtype=self.dtype, device='cpu')
849
+
850
+ dataset = CustomDataset(xs)
851
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
852
+
853
+ A = []
854
+ loc = self._codebook()
855
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
856
+ for X_batch, idx in dataloader:
857
+ X_batch = X_batch.to(self.get_device())
858
+ library_size = torch.sum(X_batch, 1, keepdim=True)
859
+
860
+ if not codebook:
861
+ z_basal = self._get_basal_embedding(X_batch)
862
+ else:
863
+ ns = self._soft_assignments(X_batch)
864
+ z_basal = ns @ loc
865
+
866
+ zct = torch.zeros_like(z_basal)
867
+ if cs_list is not None:
868
+ C_batch = cs[idx].to(self.get_device())
869
+ shift = 0
870
+ for i, context_size in enumerate(self.context_sizes):
871
+ C_batch_i = C_batch[:, shift:(shift+context_size)]
872
+ zct += self.context_tokens[i](C_batch_i)
873
+ shift += context_size
874
+ zcs = self.context_effect([z_basal,zct])
875
+ z_basal += zcs
876
+
877
+ if ps is not None:
878
+ P_batch = ps[idx].to(self.get_device())
879
+ zpt = self.perturb_token(P_batch)
880
+ zps = self.perturb_effect([z_basal,zpt])
881
+ z_basal += zps
882
+
883
+ zfs = torch.zeros(X_batch.shape[0], self.latent_dim).to(self.get_device())
884
+ if fs_list is not None:
885
+ zfs_ = torch.zeros(X_batch.shape[0], self.covariate_dim).to(self.get_device())
886
+ F_batch = fs[idx].to(self.get_device())
887
+ shift = 0
888
+ for i, covariate_size in enumerate(self.covariate_sizes):
889
+ F_batch_i = F_batch[:, shift:(shift+covariate_size)]
890
+ zfs_ += self.covariate_tokens[i](F_batch_i)#.repeat(1,self.latent_dim)
891
+ shift += covariate_size
892
+ zfs = self.covariate_effect(zfs_)
893
+
894
+ zas = self.latent_decoder(z_basal)
895
+
896
+ pathway_chunk, novel_chunk = torch.chunk(zas, chunks=[self.n_pathways, self.n_latent], dim=1)
897
+
898
+ pathway_log_mu = self.pathway_decoder_log_mu(pathway_chunk)
899
+
900
+ chunks = torch.chunk(novel_chunk, chunks=len(self.decoder_log_mu), dim=1)
901
+ log_mu_parts = [self.decoder_log_mu[i](chunk) for i, chunk in enumerate(chunks)]
902
+ log_mu = torch.stack(log_mu_parts, dim=1) # [N, E, H] (假设输出带专家维度)
903
+
904
+ total_log_mu = torch.stack([pathway_log_mu, log_mu], dim=1)
905
+ log_mu = self.lse_pool(total_log_mu)
906
+ logits = log_mu - log_mu.logsumexp(-1, keepdim=True) # log softmax
907
+ mu = logits.exp()
908
+ counts = mu * library_size
909
+
910
+ A.append(tensor_to_numpy(counts))
911
+ pbar.update(1)
912
+
913
+ A = np.concatenate(A)
914
+ return A
915
+
916
+ def preprocess(self, xs):
917
+ xs = np.round(xs)
918
+
919
+ if sparse.issparse(xs):
920
+ xs = xs.toarray()
921
+ return xs
922
+
923
+ def fit(self, xs: np.array,
924
+ css: list = None,
925
+ ps: np.array = None,
926
+ fss: list = None,
927
+ num_epochs: int = 100,
928
+ learning_rate: float = 0.0001,
929
+ use_mask: bool = False,
930
+ mask_ratio: float = 0.15,
931
+ batch_size: int = 2000,
932
+ algo: Literal['adam','rmsprop','adamw'] = 'adam',
933
+ beta_1: float = 0.9,
934
+ weight_decay: float = 0.005,
935
+ decay_rate: float = 0.9,
936
+ config_enum: str = 'parallel',
937
+ use_jax: bool = False,
938
+ show_progress: bool = True,
939
+ # Early stopping 相关参数
940
+ patience: int = 10,
941
+ min_delta: float = 1e-4,
942
+ restore_best_weights: bool = True,
943
+ monitor: str = 'loss'):
944
+ """
945
+ Train the SURENF model.
946
+
947
+ Parameters
948
+ ----------
949
+ xs
950
+ Single-cell expression matrix. It should be a Numpy array or a Pytorch Tensor.
951
+ Rows are cells and columns are features.
952
+ cs
953
+ cell-level factor matrix.
954
+ num_epochs
955
+ Number of training epochs.
956
+ learning_rate
957
+ Parameter for training.
958
+ batch_size
959
+ Size of batch processing.
960
+ algo
961
+ Optimization algorithm.
962
+ beta_1
963
+ Parameter for optimization.
964
+ weight_decay
965
+ Parameter for optimization.
966
+ decay_rate
967
+ Parameter for optimization.
968
+ patience
969
+ Number of epochs with no improvement after which training will be stopped.
970
+ min_delta
971
+ Minimum change in the monitored quantity to qualify as an improvement.
972
+ restore_best_weights
973
+ Whether to restore model weights from the epoch with the best value of the monitored quantity.
974
+ monitor
975
+ Quantity to be monitored. Currently supports 'loss'.
976
+ use_jax
977
+ If toggled on, Jax will be used for speeding up.
978
+ show_progress
979
+ Whether to show training progress bar.
980
+ """
981
+ self.use_mask = use_mask
982
+ self.mask_ratio = mask_ratio
983
+
984
+ xs = self.preprocess(xs)
985
+ xs = convert_to_tensor(xs, dtype=self.dtype, device='cpu')
986
+ if css is not None:
987
+ cs = np.hstack(css)
988
+ cs = convert_to_tensor(cs, dtype=self.dtype, device='cpu')
989
+ if ps is not None:
990
+ ps = convert_to_tensor(ps, dtype=self.dtype, device='cpu')
991
+ if fss is not None:
992
+ fs = np.hstack(fss)
993
+ fs = convert_to_tensor(fs, dtype=self.dtype, device='cpu')
994
+
995
+ dataset = CustomDataset(xs)
996
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
997
+
998
+ # setup the optimizer
999
+ optim_params = {'lr': learning_rate, 'betas': (beta_1, 0.999), 'weight_decay': weight_decay}
1000
+
1001
+ if algo.lower()=='rmsprop':
1002
+ optimizer_class = torch.optim.RMSprop
1003
+ elif algo.lower()=='adam':
1004
+ optimizer_class = torch.optim.Adam
1005
+ elif algo.lower() == 'adamw':
1006
+ optimizer_class = torch.optim.AdamW
1007
+ else:
1008
+ raise ValueError("An optimization algorithm must be specified.")
1009
+
1010
+ # 初始化优化器
1011
+ scheduler_config = {
1012
+ 'optimizer': optimizer_class,
1013
+ 'optim_args': optim_params,
1014
+ 'gamma': decay_rate
1015
+ }
1016
+
1017
+ pyro.clear_param_store()
1018
+
1019
+ # ------------- Stage 1 -----------------------------
1020
+ # set up the loss(es) for inference
1021
+ Elbo = JitTraceEnum_ELBO if use_jax else TraceEnum_ELBO
1022
+ elbo = Elbo(max_plate_nesting=1, strict_enumeration_warning=False)
1023
+ #Elbo = JitTraceEnum_ELBO if use_jax else RegTraceEnumELBO
1024
+ #elbo = Elbo(max_plate_nesting=1, strict_enumeration_warning=False, ortho_scale=lambda_orth, sparse_scale=lambda_sparse)
1025
+ guide = config_enumerate(self.guide, config_enum, expand=True)
1026
+
1027
+ # 创建 scheduler
1028
+ scheduler = ExponentialLR(scheduler_config)
1029
+ loss_basic = SVI(self.model, guide, scheduler, loss=elbo)
1030
+
1031
+ # build a list of all losses considered
1032
+ losses = [loss_basic]
1033
+ num_losses = len(losses)
1034
+
1035
+ # Early stopping 初始化
1036
+ best_loss = float('inf')
1037
+ best_epoch = 0
1038
+ patience_counter = 0
1039
+
1040
+ # 保存最佳状态
1041
+ best_state = None
1042
+
1043
+ with tqdm(total=num_epochs, disable=not show_progress, desc='Training', unit='epoch') as pbar:
1044
+ for epoch in range(num_epochs):
1045
+ epoch_losses = [0.0] * num_losses
1046
+ for batch_x, idx in dataloader:
1047
+ batch_x = batch_x.to(self.get_device())
1048
+ for loss_id in range(num_losses):
1049
+ if css is None:
1050
+ batch_c = None
1051
+ else:
1052
+ batch_c = cs[idx].to(self.get_device())
1053
+ if ps is None:
1054
+ batch_p = None
1055
+ else:
1056
+ batch_p = ps[idx].to(self.get_device())
1057
+ if fss is None:
1058
+ batch_f = None
1059
+ else:
1060
+ batch_f = fs[idx].to(self.get_device())
1061
+
1062
+ new_loss = losses[loss_id].step(xs=batch_x, cs=batch_c, ps=batch_p, fs=batch_f)
1063
+ epoch_losses[loss_id] += new_loss
1064
+
1065
+ avg_epoch_losses_ = list(map(lambda v: v / len(dataloader), epoch_losses))
1066
+ avg_epoch_losses_str = list(map(lambda v: "{:.4f}".format(v), avg_epoch_losses_))
1067
+
1068
+ # 计算当前 epoch 的总损失
1069
+ current_loss = sum(avg_epoch_losses_)
1070
+
1071
+ # Early stopping 逻辑
1072
+ if current_loss < best_loss - min_delta:
1073
+ # 有显著改进
1074
+ best_loss = current_loss
1075
+ best_epoch = epoch
1076
+ patience_counter = 0
1077
+
1078
+ # 保存最佳状态(模型参数 + 优化器状态)
1079
+ if restore_best_weights:
1080
+ best_state = self.get_model_and_optimizer_state(scheduler)
1081
+
1082
+ pbar.set_postfix({
1083
+ 'loss': ' '.join(avg_epoch_losses_str),
1084
+ 'best': f"{best_loss:.4f}",
1085
+ 'patience': f"{patience_counter}/{patience}"
1086
+ })
1087
+ else:
1088
+ # 没有改进
1089
+ patience_counter += 1
1090
+ pbar.set_postfix({
1091
+ 'loss': ' '.join(avg_epoch_losses_str),
1092
+ 'best': f"{best_loss:.4f}",
1093
+ 'patience': f"{patience_counter}/{patience}"
1094
+ })
1095
+
1096
+ # 检查是否应该提前停止
1097
+ if patience_counter >= patience:
1098
+ print(f"\nEarly stopping triggered at epoch {epoch + 1}")
1099
+ print(f"Best loss {best_loss:.4f} achieved at epoch {best_epoch + 1}")
1100
+
1101
+ # 恢复最佳状态
1102
+ if restore_best_weights and best_state is not None:
1103
+ self.load_model_and_optimizer_state(best_state, scheduler)
1104
+ print("Restored model and optimizer states from best epoch.")
1105
+
1106
+ break
1107
+
1108
+ pbar.update(1)
1109
+
1110
+ # 训练完成(达到最大 epoch 数)
1111
+ if epoch == num_epochs - 1:
1112
+ print(f"\nTraining completed. Best loss {best_loss:.4f} at epoch {best_epoch + 1}")
1113
+ if restore_best_weights and best_state is not None:
1114
+ self.load_model_and_optimizer_state(best_state, scheduler)
1115
+ print("Restored model and optimizer states from best epoch.")
1116
+
1117
+ def get_model_and_optimizer_state(self, scheduler=None):
1118
+ """获取模型和优化器的完整状态"""
1119
+ state_dict = {
1120
+ 'model_params': dict(pyro.get_param_store()), # 保存所有参数
1121
+ }
1122
+
1123
+ # 保存优化器状态
1124
+ if scheduler is not None and hasattr(scheduler, 'optimizer'):
1125
+ # Pyro scheduler 通常包含 optimizer
1126
+ optimizer = scheduler.optimizer
1127
+ if optimizer is not None:
1128
+ state_dict['optimizer_state'] = optimizer.state_dict()
1129
+
1130
+ # 保存 scheduler 状态
1131
+ if scheduler is not None and hasattr(scheduler, 'scheduler'):
1132
+ scheduler_obj = scheduler.scheduler
1133
+ if scheduler_obj is not None and hasattr(scheduler_obj, 'state_dict'):
1134
+ state_dict['scheduler_state'] = scheduler_obj.state_dict()
1135
+
1136
+ return state_dict
1137
+
1138
+ def load_model_and_optimizer_state(self, state_dict, scheduler=None):
1139
+ """加载模型和优化器的完整状态"""
1140
+
1141
+ # 1. 加载模型参数
1142
+ if 'model_params' in state_dict:
1143
+ param_store = pyro.get_param_store()
1144
+ for name, value in state_dict['model_params'].items():
1145
+ # 确保参数存在
1146
+ if name in param_store:
1147
+ # 创建可训练的参数
1148
+ param_store[name] = value.clone().detach().requires_grad_(True)
1149
+ else:
1150
+ # 如果参数不存在,则创建它
1151
+ param_store[name] = torch.nn.Parameter(
1152
+ value.clone().detach().requires_grad_(True)
1153
+ )
1154
+
1155
+ # 2. 加载优化器状态
1156
+ if scheduler is not None and 'optimizer_state' in state_dict:
1157
+ optimizer = scheduler.optimizer
1158
+ if optimizer is not None:
1159
+ # 确保优化器的参数与当前模型参数匹配
1160
+ current_params = dict(pyro.get_param_store())
1161
+
1162
+ # 创建一个新的状态字典,只包含当前存在的参数
1163
+ filtered_optimizer_state = {
1164
+ 'state': {},
1165
+ 'param_groups': state_dict['optimizer_state']['param_groups']
1166
+ }
1167
+
1168
+ # 过滤状态,只保留当前存在的参数
1169
+ for param_idx, param_state in state_dict['optimizer_state']['state'].items():
1170
+ # 检查参数是否仍然存在
1171
+ param_key = f'Param_{param_idx}'
1172
+ if param_idx < len(optimizer.param_groups[0]['params']):
1173
+ filtered_optimizer_state['state'][param_idx] = param_state
1174
+
1175
+ optimizer.load_state_dict(filtered_optimizer_state)
1176
+
1177
+ # 3. 加载 scheduler 状态
1178
+ if (scheduler is not None and
1179
+ 'scheduler_state' in state_dict and
1180
+ hasattr(scheduler, 'scheduler')):
1181
+ scheduler_obj = scheduler.scheduler
1182
+ if (scheduler_obj is not None and
1183
+ hasattr(scheduler_obj, 'load_state_dict')):
1184
+ scheduler_obj.load_state_dict(state_dict['scheduler_state'])
1185
+
1186
+ def get_optimizer_param_mapping(self, optimizer):
1187
+ """获取优化器参数到模型参数的映射关系"""
1188
+ if optimizer is None:
1189
+ return {}
1190
+
1191
+ mapping = {}
1192
+ param_store = dict(pyro.get_param_store())
1193
+
1194
+ for group_idx, param_group in enumerate(optimizer.param_groups):
1195
+ for param_idx, param in enumerate(param_group['params']):
1196
+ # 找到对应的模型参数名
1197
+ for name, model_param in param_store.items():
1198
+ if param is model_param or torch.equal(param, model_param):
1199
+ mapping[f'group{group_idx}_param{param_idx}'] = name
1200
+ break
1201
+
1202
+ return mapping
1203
+
1204
+ # 使用自动微分计算 Jacobian
1205
+ def compute_jacobian(self, zs):
1206
+ """
1207
+ zs: torch.Tensor of shape (B, latent_dim)
1208
+ returns: Jacobian of shape (B, input_dim, latent_dim)
1209
+ """
1210
+ zs = convert_to_tensor(zs, dtype=self.dtype, device=self.get_device())
1211
+ zs.requires_grad_(True)
1212
+ mu = self._decode_latent(zs)
1213
+
1214
+ jacobians = []
1215
+ for i in range(mu.shape[-1]):
1216
+ grad = torch.autograd.grad(
1217
+ mu[:, i].sum(), zs,
1218
+ create_graph=True,
1219
+ retain_graph=True
1220
+ )[0] # (B, d)
1221
+ jacobians.append(grad.unsqueeze(1))
1222
+
1223
+ return tensor_to_numpy(torch.cat(jacobians, dim=1)) # (B, input_dim, d)
1224
+
1225
+ def delta_z(self, J, delta_x, rcond=1e-6):
1226
+ """
1227
+ J: (input_dim, latent_dim)
1228
+ delta_x: (input_dim,)
1229
+ return: delta_z (latent_dim,)
1230
+ """
1231
+ J = convert_to_tensor(J, dtype=self.dtype, device=self.get_device())
1232
+ delta_x = convert_to_tensor(delta_x, dtype=self.dtype, device=self.get_device())
1233
+ JtJ = J.T @ J # (latent_dim, latent_dim)
1234
+ JtJ_inv = torch.linalg.pinv(JtJ, rcond=rcond)
1235
+ delta_z = JtJ_inv @ J.T @ delta_x
1236
+ return tensor_to_numpy(delta_z)
1237
+
1238
+ @classmethod
1239
+ def save_model(cls, model, file_path, compression=False):
1240
+ """Save the model to the specified file path."""
1241
+ file_path = os.path.abspath(file_path)
1242
+
1243
+ model.eval()
1244
+ if compression:
1245
+ with gzip.open(file_path, 'wb') as pickle_file:
1246
+ pickle.dump(model, pickle_file)
1247
+ else:
1248
+ with open(file_path, 'wb') as pickle_file:
1249
+ pickle.dump(model, pickle_file)
1250
+
1251
+ print(f'Model saved to {file_path}')
1252
+
1253
+ @classmethod
1254
+ def load_model(cls, file_path):
1255
+ """Load the model from the specified file path and return an instance."""
1256
+ print(f'Model loaded from {file_path}')
1257
+
1258
+ file_path = os.path.abspath(file_path)
1259
+ if file_path.endswith('gz'):
1260
+ with gzip.open(file_path, 'rb') as pickle_file:
1261
+ model = pickle.load(pickle_file)
1262
+ else:
1263
+ with open(file_path, 'rb') as pickle_file:
1264
+ model = pickle.load(pickle_file)
1265
+
1266
+ return model
1267
+
1268
+
1269
+
1270
+
1271
+ # decoder_flow
1272
+
1273
+ from pyro.distributions.transforms import Planar as PlanarFlow
1274
+ from pyro.distributions.transforms import Radial as RadialFlow
1275
+ from pyro.nn import PyroModule
1276
+ from pyro.distributions.transforms import ComposeTransform
1277
+
1278
+
1279
+ class NormalizeFlow(PyroModule):
1280
+ def __init__(
1281
+ self,
1282
+ z_dim: int,
1283
+ num_flows: int = 2,
1284
+ flow_type: Literal['planar','radial'] = 'planar',
1285
+ ):
1286
+ super().__init__()
1287
+ self.z_dim = z_dim
1288
+ if flow_type == 'planar':
1289
+ self.flows = torch.nn.ModuleList(
1290
+ [PlanarFlow(z_dim) for _ in range(num_flows)]
1291
+ )
1292
+ elif flow_type == 'radial':
1293
+ self.flows = torch.nn.ModuleList(
1294
+ [RadialFlow(z_dim) for _ in range(num_flows)]
1295
+ )
1296
+ else:
1297
+ raise ValueError(f"Unknown flow_type: {flow_type}")
1298
+
1299
+ def transforms(self):
1300
+ """返回 ComposeTransform,供 TransformedDistribution 使用"""
1301
+ return ComposeTransform(list(self.flows))
1302
+
1303
+ def forward(self, z):
1304
+ """
1305
+ z: (batch, z_dim)
1306
+ returns z': same shape, after flow transforms
1307
+ """
1308
+ for flow in self.flows:
1309
+ z = flow(z)
1310
+ return z