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 +1310 -0
- PathwayVI/__init__.py +8 -0
- PathwayVI/dist/__init__.py +1 -0
- PathwayVI/dist/negbinomial.py +415 -0
- PathwayVI/gmt/__init__.py +1 -0
- PathwayVI/gmt/gmt.py +193 -0
- PathwayVI/utils/__init__.py +20 -0
- PathwayVI/utils/custom_mlp.py +409 -0
- PathwayVI/utils/label.py +215 -0
- PathwayVI/utils/queue.py +50 -0
- PathwayVI/utils/utils.py +308 -0
- pathwayvi-0.1.0.dist-info/METADATA +61 -0
- pathwayvi-0.1.0.dist-info/RECORD +15 -0
- pathwayvi-0.1.0.dist-info/WHEEL +5 -0
- pathwayvi-0.1.0.dist-info/top_level.txt +1 -0
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
|