DNF-tool 0.1.0__tar.gz

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.
@@ -0,0 +1,1058 @@
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
+
13
+ import zuko
14
+ from pyro.contrib.zuko import ZukoToPyro
15
+
16
+ import argparse
17
+ import os
18
+ import time as tm
19
+ import random
20
+ import itertools
21
+ import pandas as pd
22
+ import numpy as np
23
+ import datatable as dt
24
+ import networkx as nx
25
+ from pathlib import Path
26
+ import warnings
27
+ warnings.filterwarnings("ignore")
28
+
29
+ from .utils.utils import convert_to_tensor, tensor_to_numpy, CustomDataset, CustomDataset2, CustomDataset4
30
+ from .utils.custom_mlp import MLP, Exp
31
+ from .graph.graph import compute_metacell_diffusion_kernel,visualize_metacell_igraph_with_fa2
32
+ from .atac import binarize
33
+
34
+ from .dist.negbinomial import NegativeBinomial as MyNB
35
+ from .dist.negbinomial import ZeroInflatedNegativeBinomial as MyZINB
36
+
37
+ from tqdm import tqdm
38
+ from typing import Literal
39
+ import dill as pickle
40
+ import gzip
41
+ import scanpy as sc
42
+ from scipy import sparse
43
+
44
+ def set_random_seed(seed):
45
+ # Set seed for PyTorch
46
+ torch.manual_seed(seed)
47
+
48
+ # If using CUDA, set the seed for CUDA
49
+ if torch.cuda.is_available():
50
+ torch.cuda.manual_seed(seed)
51
+ torch.cuda.manual_seed_all(seed) # For multi-GPU setups.
52
+
53
+ # Set seed for NumPy
54
+ np.random.seed(seed)
55
+
56
+ # Set seed for Python's random module
57
+ random.seed(seed)
58
+
59
+ # Set seed for Pyro
60
+ pyro.set_rng_seed(seed)
61
+
62
+ class DNF(nn.Module):
63
+ def __init__(self,
64
+ input_size: int,
65
+ codebook_size: int = 10, # size of metacell codebook
66
+ covariate_sizes: list = [0],
67
+ covariate_dim: int = None,
68
+ z_dim: int = 50, # dimension of a metacell variable
69
+ loss_func: Literal['negbinomial','poisson','multinomial','bernoulli'] = 'poisson',
70
+ flow_transforms: int = 10,
71
+ flow_hidden_layers: list = [1024],
72
+ hidden_layers: list =[1024],
73
+ hidden_layer_activation: Literal['relu','softplus','leakyrelu','linear'] = 'relu',
74
+ inverse_dispersion: float = 10.0,
75
+ use_zeroinflate: bool = True,
76
+ nn_dropout: float = 0.1,
77
+ post_layer_fct: list = ['layernorm'],
78
+ post_act_fct: list = None,
79
+ config_enum: str = 'parallel',
80
+ use_cuda: bool = True,
81
+ seed: int = 42,
82
+ dtype = torch.float32, # type: ignore
83
+ ):
84
+ super().__init__()
85
+
86
+ # initialize the class with all arguments provided to the constructor
87
+ self.input_size = input_size
88
+ self.covariate_sizes = covariate_sizes
89
+ self.covariate_dim = z_dim if covariate_dim is None else covariate_dim
90
+ self.inverse_dispersion = inverse_dispersion
91
+ self.z_dim = z_dim
92
+ self.hidden_layers = hidden_layers
93
+ self.decoder_hidden_layers = hidden_layers[::-1]
94
+ self.allow_broadcast = config_enum == 'parallel'
95
+ self.use_cuda = use_cuda
96
+ self.loss_func = loss_func
97
+ self.options = None
98
+ self.codebook_size=codebook_size
99
+ self.G = None
100
+ self.dtype = dtype
101
+ self.normalize = True
102
+ self.use_zeroinflate=use_zeroinflate
103
+
104
+ self.transforms = flow_transforms
105
+ self.flow_hidden_layers = flow_hidden_layers
106
+
107
+ self.nn_dropout = nn_dropout
108
+ self.post_layer_fct = post_layer_fct
109
+ self.post_act_fct = post_act_fct
110
+ self.hidden_layer_activation = hidden_layer_activation
111
+
112
+ assert loss_func in ['poisson','multinomial','negbinomial','bernoulli']
113
+
114
+ if seed is not None:
115
+ set_random_seed(seed)
116
+
117
+ # define and instantiate the neural networks representing
118
+ # the parameters of various distributions in the model
119
+ self.setup_networks()
120
+
121
+ def setup_networks(self):
122
+ z_dim = self.z_dim
123
+ hidden_sizes = self.hidden_layers
124
+
125
+ nn_layer_norm, nn_batch_norm, nn_layer_dropout = False, False, False
126
+ na_layer_norm, na_batch_norm, na_layer_dropout = False, False, False
127
+
128
+ if self.post_layer_fct is not None:
129
+ nn_layer_norm=True if ('layernorm' in self.post_layer_fct) or ('layer_norm' in self.post_layer_fct) else False
130
+ nn_batch_norm=True if ('batchnorm' in self.post_layer_fct) or ('batch_norm' in self.post_layer_fct) else False
131
+ nn_layer_dropout=True if 'dropout' in self.post_layer_fct else False
132
+
133
+ if self.post_act_fct is not None:
134
+ na_layer_norm=True if ('layernorm' in self.post_act_fct) or ('layer_norm' in self.post_act_fct) else False
135
+ na_batch_norm=True if ('batchnorm' in self.post_act_fct) or ('batch_norm' in self.post_act_fct) else False
136
+ na_layer_dropout=True if 'dropout' in self.post_act_fct else False
137
+
138
+ if nn_layer_norm and nn_batch_norm and nn_layer_dropout:
139
+ 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))
140
+ elif nn_layer_norm and nn_layer_dropout:
141
+ post_layer_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.Dropout(self.nn_dropout), nn.LayerNorm(layer.module.out_features))
142
+ elif nn_batch_norm and nn_layer_dropout:
143
+ post_layer_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.Dropout(self.nn_dropout), nn.BatchNorm1d(layer.module.out_features))
144
+ elif nn_layer_norm and nn_batch_norm:
145
+ post_layer_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.BatchNorm1d(layer.module.out_features), nn.LayerNorm(layer.module.out_features))
146
+ elif nn_layer_norm:
147
+ post_layer_fct = lambda layer_ix, total_layers, layer: nn.LayerNorm(layer.module.out_features)
148
+ elif nn_batch_norm:
149
+ post_layer_fct = lambda layer_ix, total_layers, layer:nn.BatchNorm1d(layer.module.out_features)
150
+ elif nn_layer_dropout:
151
+ post_layer_fct = lambda layer_ix, total_layers, layer: nn.Dropout(self.nn_dropout)
152
+ else:
153
+ post_layer_fct = lambda layer_ix, total_layers, layer: None
154
+
155
+ if na_layer_norm and na_batch_norm and na_layer_dropout:
156
+ 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))
157
+ elif na_layer_norm and na_layer_dropout:
158
+ post_act_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.Dropout(self.nn_dropout), nn.LayerNorm(layer.module.out_features))
159
+ elif na_batch_norm and na_layer_dropout:
160
+ post_act_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.Dropout(self.nn_dropout), nn.BatchNorm1d(layer.module.out_features))
161
+ elif na_layer_norm and na_batch_norm:
162
+ post_act_fct = lambda layer_ix, total_layers, layer: nn.Sequential(nn.BatchNorm1d(layer.module.out_features), nn.LayerNorm(layer.module.out_features))
163
+ elif na_layer_norm:
164
+ post_act_fct = lambda layer_ix, total_layers, layer: nn.LayerNorm(layer.module.out_features)
165
+ elif na_batch_norm:
166
+ post_act_fct = lambda layer_ix, total_layers, layer:nn.BatchNorm1d(layer.module.out_features)
167
+ elif na_layer_dropout:
168
+ post_act_fct = lambda layer_ix, total_layers, layer: nn.Dropout(self.nn_dropout)
169
+ else:
170
+ post_act_fct = lambda layer_ix, total_layers, layer: None
171
+
172
+ if self.hidden_layer_activation == 'relu':
173
+ activate_fct = nn.ReLU
174
+ elif self.hidden_layer_activation == 'softplus':
175
+ activate_fct = nn.Softplus
176
+ elif self.hidden_layer_activation == 'leakyrelu':
177
+ activate_fct = nn.LeakyReLU
178
+ elif self.hidden_layer_activation == 'linear':
179
+ activate_fct = nn.Identity
180
+
181
+ # define the neural networks used later in the model and the guide.
182
+ self.encoder_n = MLP(
183
+ [self.z_dim] + hidden_sizes + [self.codebook_size],
184
+ activation=activate_fct,
185
+ output_activation=None,
186
+ post_layer_fct=post_layer_fct,
187
+ post_act_fct=post_act_fct,
188
+ allow_broadcast=self.allow_broadcast,
189
+ use_cuda=self.use_cuda,
190
+ )
191
+
192
+ self.encoder_zn = MLP(
193
+ [self.input_size] + hidden_sizes + [[z_dim, z_dim]],
194
+ activation=activate_fct,
195
+ output_activation=[None, Exp],
196
+ post_layer_fct=post_layer_fct,
197
+ post_act_fct=post_act_fct,
198
+ allow_broadcast=self.allow_broadcast,
199
+ use_cuda=self.use_cuda,
200
+ )
201
+
202
+ if np.sum(self.covariate_sizes)>0:
203
+ self.covariate_tokens = nn.ModuleList()
204
+ for covariate_size in self.covariate_sizes:
205
+ self.covariate_tokens.append(MLP(
206
+ [covariate_size] + self.decoder_hidden_layers + [self.covariate_dim],
207
+ activation=activate_fct,
208
+ output_activation=None,
209
+ post_layer_fct=post_layer_fct,
210
+ post_act_fct=post_act_fct,
211
+ allow_broadcast=self.allow_broadcast,
212
+ use_cuda=self.use_cuda,
213
+ )
214
+ )
215
+ self.covariate_effect = MLP(
216
+ [self.covariate_dim] + self.decoder_hidden_layers + [self.z_dim],
217
+ activation=activate_fct,
218
+ output_activation=None,
219
+ post_layer_fct=post_layer_fct,
220
+ post_act_fct=post_act_fct,
221
+ allow_broadcast=self.allow_broadcast,
222
+ use_cuda=self.use_cuda,
223
+ )
224
+
225
+ self.decoder_log_mu = MLP(
226
+ [self.latent_dim+self.latent_dim] + self.decoder_hidden_layers + [self.input_dim],
227
+ #activation=activate_fct,
228
+ activation=nn.Identity,
229
+ output_activation=None,
230
+ #post_layer_fct=post_layer_fct,
231
+ #post_act_fct=post_act_fct,
232
+ allow_broadcast=self.allow_broadcast,
233
+ use_cuda=self.use_cuda,
234
+ )
235
+ '''self.decoder_log_mu = zuko.flows.NSF(features=self.input_size, context=self.z_dim + self.z_dim,
236
+ transforms=self.transforms,
237
+ hidden_features=self.flow_hidden_layers)'''
238
+
239
+ self.latent_decoder = zuko.flows.NSF(features=self.z_dim, context=self.z_dim,
240
+ transforms=self.transforms,
241
+ hidden_features=self.flow_hidden_layers)
242
+
243
+ self.codebook = MLP(
244
+ [self.codebook_size] + hidden_sizes + [self.z_dim],
245
+ activation=activate_fct,
246
+ output_activation=None,
247
+ post_layer_fct=post_layer_fct,
248
+ post_act_fct=post_act_fct,
249
+ allow_broadcast=self.allow_broadcast,
250
+ use_cuda=self.use_cuda,
251
+ )
252
+
253
+ # using GPUs for faster training of the networks
254
+ if self.use_cuda:
255
+ self.cuda()
256
+
257
+ def cutoff(self, xs, thresh=None):
258
+ eps = torch.finfo(xs.dtype).eps
259
+
260
+ if not thresh is None:
261
+ if eps < thresh:
262
+ eps = thresh
263
+
264
+ xs = xs.clamp(min=eps)
265
+
266
+ if torch.any(torch.isnan(xs)):
267
+ xs[torch.isnan(xs)] = eps
268
+
269
+ return xs
270
+
271
+ def softmax(self, xs):
272
+ #soft_enc = nn.Softmax(dim=1)
273
+ #xs = soft_enc(xs)
274
+ #xs = clamp_probs(xs)
275
+ #xs = ft.normalize(xs, 1, 1)
276
+ xs = SoftmaxTransform()(xs)
277
+ return xs
278
+
279
+ def sigmoid(self, xs):
280
+ sigm_enc = nn.Sigmoid()
281
+ xs = sigm_enc(xs)
282
+ xs = clamp_probs(xs)
283
+ return xs
284
+
285
+ def softmax_logit(self, xs):
286
+ eps = torch.finfo(xs.dtype).eps
287
+ xs = self.softmax(xs)
288
+ xs = torch.logit(xs, eps=eps)
289
+ return xs
290
+
291
+ def logit(self, xs):
292
+ eps = torch.finfo(xs.dtype).eps
293
+ xs = torch.logit(xs, eps=eps)
294
+ return xs
295
+
296
+ def dirimulti_param(self, xs):
297
+ xs = self.dirimulti_mass * self.sigmoid(xs)
298
+ return xs
299
+
300
+ def multi_param(self, xs):
301
+ xs = self.softmax(xs)
302
+ return xs
303
+
304
+ def get_device(self):
305
+ return next(self.parameters()).device
306
+
307
+ def model(self, xs, fs=None):
308
+ # register this pytorch module and all of its sub-modules with pyro
309
+ pyro.module('DNF', self)
310
+
311
+ eps = torch.finfo(xs.dtype).eps
312
+ batch_size = xs.size(0)
313
+ self.options = dict(dtype=xs.dtype, device=xs.device)
314
+
315
+ if self.loss_func == 'negbinomial':
316
+ dispersion = pyro.param("inverse_dispersion", self.inverse_dispersion * torch.ones(1, self.input_size, **self.options),
317
+ constraint=constraints.positive)
318
+ if self.use_zeroinflate:
319
+ gate_logits = pyro.param("dropout_rate", xs.new_zeros(self.input_size))
320
+
321
+ I = torch.eye(self.codebook_size)
322
+ acs_loc = self.codebook(I)
323
+
324
+ with pyro.plate('data'):
325
+ ###############################################
326
+ # p(zn)
327
+ prior = torch.zeros(batch_size, self.codebook_size, **self.options)
328
+ ns = pyro.sample('n', dist.OneHotCategorical(logits=prior))
329
+ _, ind = torch.topk(ns, 1)
330
+
331
+ zn_loc = acs_loc[ind.squeeze()]
332
+ zns = pyro.sample('zn', ZukoToPyro(self.latent_decoder(zn_loc)))
333
+
334
+ ###############################################
335
+ if (np.sum(self.covariate_sizes)>0) and (fs is not None):
336
+ zfs_ = torch.zeros(batch_size, self.covariate_dim).to(self.get_device())
337
+ shift = 0
338
+ for i, covariate_size in enumerate(self.covariate_sizes):
339
+ fs_i = fs[:,shift:(shift+covariate_size)]
340
+ zfs_ += self.covariate_tokens[i](fs_i)#.repeat(1,self.latent_dim)
341
+ shift += covariate_size
342
+ zfs = self.covariate_effect(zfs_)
343
+ else:
344
+ zfs = torch.zeros(batch_size, self.latent_dim).to(self.get_device())
345
+
346
+ ###############################################
347
+ log_mu = self.decoder_log_mu([zns,zfs])
348
+ if self.loss_func in ['bernoulli']:
349
+ log_theta = log_mu
350
+ elif self.loss_func in ['negbinomial']:
351
+ mu = log_mu.exp()
352
+ else:
353
+ rate = log_mu.exp()
354
+ theta = dist.DirichletMultinomial(total_count=1, concentration=rate).mean
355
+ if self.loss_func == 'poisson':
356
+ rate = theta * torch.sum(xs, dim=1, keepdim=True)
357
+
358
+ if self.loss_func == 'negbinomial':
359
+ if self.use_zeroinflate:
360
+ pyro.sample("x", MyZINB(mu=mu, theta=dispersion, zi_logits=gate_logits).to_event(1), obs=xs)
361
+ else:
362
+ pyro.sample("x", MyNB(mu=mu, theta=dispersion).to_event(1), obs=xs)
363
+ elif self.loss_func == 'poisson':
364
+ if self.use_zeroinflate:
365
+ pyro.sample('x', dist.ZeroInflatedDistribution(dist.Poisson(rate=rate),gate_logits=gate_logits).to_event(1), obs=xs.round())
366
+ else:
367
+ pyro.sample('x', dist.Poisson(rate=rate).to_event(1), obs=xs.round())
368
+ elif self.loss_func == 'multinomial':
369
+ pyro.sample('x', dist.Multinomial(total_count=int(1e8), probs=theta), obs=xs)
370
+ elif self.loss_func == 'bernoulli':
371
+ if self.use_zeroinflate:
372
+ pyro.sample('x', dist.ZeroInflatedDistribution(dist.Bernoulli(logits=log_theta),gate_logits=gate_logits).to_event(1), obs=xs)
373
+ else:
374
+ pyro.sample('x', dist.Bernoulli(logits=log_theta).to_event(1), obs=xs)
375
+
376
+ def guide(self, xs, fs=None):
377
+ # inform Pyro that the variables in the batch of xs, ys are conditionally independent
378
+ with pyro.plate('data'):
379
+ # q(zn | x)
380
+ zn_loc, zn_scale = self.encoder_zn(xs)
381
+ zns = pyro.sample('zn', dist.Normal(zn_loc, zn_scale).to_event(1))
382
+
383
+ alpha = self.encoder_n(zns)
384
+ ns = pyro.sample('n', dist.OneHotCategorical(logits=alpha))
385
+
386
+ def _code(self, xs):
387
+ zns,_ = self.encoder_zn(xs)
388
+ alpha = self.encoder_n(zns)
389
+ return alpha
390
+
391
+ def _cell_embedding(self, xs):
392
+ zs,_ = self.encoder_zn(xs)
393
+ return zs
394
+
395
+ def get_cell_embedding(self,xs,batch_size=1024):
396
+ xs = self.preprocess(xs)
397
+ xs = convert_to_tensor(xs, device=self.get_device())
398
+ dataset = CustomDataset(xs)
399
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
400
+
401
+ Z = []
402
+ with tqdm(total=len(dataloader), desc='', unit='batch') as pbar:
403
+ for X_batch, _ in dataloader:
404
+ zns = self._cell_embedding(X_batch)
405
+ Z.append(tensor_to_numpy(zns))
406
+ pbar.update(1)
407
+
408
+ Z = np.concatenate(Z)
409
+ return Z
410
+
411
+ def _soft_assignments(self, xs):
412
+ alpha = self._code(xs)
413
+ alpha = self.softmax(alpha)
414
+ return alpha
415
+
416
+ def soft_assignments(self, xs, batch_size=1024):
417
+ xs = self.preprocess(xs)
418
+ xs = convert_to_tensor(xs, device=self.get_device())
419
+ dataset = CustomDataset(xs)
420
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
421
+
422
+ A = []
423
+ with tqdm(total=len(dataloader), desc='', unit='batch') as pbar:
424
+ for X_batch, _ in dataloader:
425
+ a = self._soft_assignments(X_batch)
426
+ A.append(tensor_to_numpy(a))
427
+ pbar.update(1)
428
+
429
+ A = np.concatenate(A)
430
+ return A
431
+
432
+ def _hard_assignments(self, xs):
433
+ alpha = self._code(xs)
434
+ res, ind = torch.topk(alpha, 1)
435
+ ns = torch.zeros_like(alpha).scatter_(1, ind, 1.0)
436
+ return ns
437
+
438
+ def hard_assignments(self, xs, batch_size=1024):
439
+ xs = self.preprocess(xs)
440
+ xs = convert_to_tensor(xs, device=self.get_device())
441
+ dataset = CustomDataset(xs)
442
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
443
+
444
+ A = []
445
+ with tqdm(total=len(dataloader), desc='', unit='batch') as pbar:
446
+ for X_batch, _ in dataloader:
447
+ a = self._hard_assignments(X_batch)
448
+ A.append(tensor_to_numpy(a))
449
+ pbar.update(1)
450
+
451
+ A = np.concatenate(A)
452
+ return A
453
+
454
+ def _log_prob(self,xs):
455
+ zs = self.encoder_zn(xs)
456
+ qz = self.latent_decoder(zs)
457
+ return qz.log_prob(xs)
458
+
459
+ def log_prob(self, xs, batch_size=1024):
460
+ xs = self.preprocess(xs)
461
+ xs = convert_to_tensor(xs, device=self.get_device())
462
+ dataset = CustomDataset(xs)
463
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
464
+
465
+ A = []
466
+ with tqdm(total=len(dataloader), desc='', unit='batch') as pbar:
467
+ for X_batch, _ in dataloader:
468
+ a = self._log_prob(X_batch)
469
+ A.append(tensor_to_numpy(a))
470
+ pbar.update(1)
471
+
472
+ A = np.concatenate(A)
473
+ return A
474
+
475
+ def _soft_assignments(self, xs):
476
+ alpha = self._code(xs)
477
+ alpha = self.softmax(alpha)
478
+ return alpha
479
+
480
+ def soft_assignments(self, xs, batch_size=1024, show_progress=True):
481
+ """
482
+ Map cells to metacells and return the probabilistic values of metacell assignments
483
+ """
484
+ xs = self.preprocess(xs)
485
+ xs = convert_to_tensor(xs, device='cpu')
486
+ dataset = CustomDataset(xs)
487
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
488
+
489
+ A = []
490
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
491
+ for X_batch, _ in dataloader:
492
+ X_batch = X_batch.to(self.get_device())
493
+ a = self._soft_assignments(X_batch)
494
+ A.append(tensor_to_numpy(a))
495
+ pbar.update(1)
496
+
497
+ A = np.concatenate(A)
498
+ return A
499
+
500
+ def _hard_assignments(self, xs):
501
+ alpha = self._code(xs)
502
+ _, ind = torch.topk(alpha, 1)
503
+ ns = torch.zeros_like(alpha).scatter_(1, ind, 1.0)
504
+ return ns,ind
505
+
506
+ def hard_assignments(self, xs, batch_size=1024, show_progress=True):
507
+ """
508
+ Map cells to metacells and return the assigned metacell identities.
509
+ """
510
+ xs = self.preprocess(xs)
511
+ xs = convert_to_tensor(xs, device='cpu')
512
+ dataset = CustomDataset(xs)
513
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False)
514
+
515
+ A = []
516
+ with tqdm(total=len(dataloader), disable=not show_progress, desc='', unit='batch') as pbar:
517
+ for X_batch, _ in dataloader:
518
+ X_batch = X_batch.to(self.get_device())
519
+ a,_ = self._hard_assignments(X_batch)
520
+ A.append(tensor_to_numpy(a))
521
+ pbar.update(1)
522
+
523
+ A = np.concatenate(A)
524
+ return A
525
+
526
+ def mmd_gaussian(self, x, y, sigma=None):
527
+ """
528
+ Compute MMD with Gaussian kernel between samples x and y.
529
+
530
+ Args:
531
+ x: Tensor of shape (n_samples, n_features)
532
+ y: Tensor of shape (m_samples, n_features)
533
+ sigma: Bandwidth of the Gaussian kernel. If None, uses median heuristic.
534
+
535
+ Returns:
536
+ mmd: Squared MMD value.
537
+ """
538
+ n, m = x.size(0), y.size(0)
539
+
540
+ if sigma is None:
541
+ # Median heuristic for bandwidth
542
+ xy = torch.cat([x, y], dim=0)
543
+ pairwise_dist = torch.cdist(xy, xy, p=2)
544
+ sigma = torch.median(pairwise_dist[pairwise_dist > 0]).detach()
545
+
546
+ # Kernel matrices
547
+ xx = torch.exp(-torch.cdist(x, x, p=2)**2 / (2 * sigma**2))
548
+ yy = torch.exp(-torch.cdist(y, y, p=2)**2 / (2 * sigma**2))
549
+ xy = torch.exp(-torch.cdist(x, y, p=2)**2 / (2 * sigma**2))
550
+
551
+ # Compute MMD
552
+ mmd = (xx.sum() - xx.diag().sum()) / (n * (n - 1)) + \
553
+ (yy.sum() - yy.diag().sum()) / (m * (m - 1)) - \
554
+ 2 * xy.mean()
555
+ return mmd
556
+
557
+ def sinkhorn_distance(self, x, y, epsilon=0.01, max_iters=100):
558
+ """
559
+ Compute regularized Wasserstein distance using Sinkhorn algorithm.
560
+
561
+ Args:
562
+ x: Tensor of shape (n, d)
563
+ y: Tensor of shape (m, d)
564
+ epsilon: Regularization parameter
565
+ max_iters: Number of Sinkhorn iterations
566
+
567
+ Returns:
568
+ wasserstein: Approximated Wasserstein distance.
569
+ """
570
+ n, m = x.size(0), y.size(0)
571
+ C = torch.cdist(x, y, p=2)**2 # Cost matrix
572
+
573
+ # Initialize dual variables
574
+ u, v = torch.zeros(n, device=x.device), torch.zeros(m, device=y.device)
575
+
576
+ for _ in range(max_iters):
577
+ u = (torch.logsumexp((C - v[None, :]) / epsilon, dim=1)) / (1 / epsilon)
578
+ v = (torch.logsumexp((C - u[:, None]) / epsilon, dim=0)) / (1 / epsilon)
579
+
580
+ # Compute transport plan and distance
581
+ P = torch.exp((u[:, None] + v[None, :] - C) / epsilon)
582
+ return torch.sum(P * C)
583
+
584
+ def metacell_distance(self, xs, ys=None, metric:Literal['mmd','sinkhorn']='mmd', epsilon:float=0.01, max_iters:int=100):
585
+ n_metacells = ys.shape[1]
586
+ dm2m = torch.zeros(n_metacells, n_metacells)
587
+ mc = np.argmax(ys, axis=1)
588
+
589
+ xs = convert_to_tensor(xs, device=self.get_device())
590
+ combinations = itertools.product(np.arange(n_metacells), np.arange(n_metacells))
591
+ for i,j in combinations:
592
+ i_cells = np.where(mc==i)
593
+ j_cells = np.where(mc==j)
594
+ if metric=='mmd':
595
+ dm2m[i,j] = self.mmd_gaussian(xs[i_cells],xs[j_cells])
596
+ else:
597
+ dm2m[i,j] = self.sinkhorn_distance(xs[i_cells],xs[j_cells],epsilon=epsilon,max_iters=max_iters)
598
+
599
+ return tensor_to_numpy(dm2m)
600
+
601
+ def metacell_similarity(self, xs, ys=None, embed: Literal['l1','l2']='l1', n_neighbors=50, sigma=1, use_diffuse=False, diffusion_time=1):
602
+ if ys is None:
603
+ ys = self.soft_assignments(xs)
604
+ if not use_diffuse:
605
+ ys = convert_to_tensor(ys, device=self.get_device())
606
+ m2m = torch.matmul(ys.T / torch.sum(ys.T, dim=1, keepdim=True), ys)
607
+ m2m = tensor_to_numpy(m2m)
608
+ else:
609
+ if embed=='l1':
610
+ zs = self.get_l1_embedding(xs)
611
+ else:
612
+ zs = self.get_l2_embedding(xs)
613
+ m2m = compute_metacell_diffusion_kernel(zs, ys, n_neighbors=n_neighbors, sigma=sigma, diffusion_time=diffusion_time)
614
+ return m2m
615
+
616
+ def metacell_network(self, affinity_matrix,
617
+ #xs, ys=None,
618
+ #k=10,
619
+ exclude_metacells: list = None):
620
+ #affinity_matrix = self.metacell_similarity(xs, ys, use_diffuse=use_diffuse)
621
+ self.G = nx.Graph()
622
+ self.G.add_nodes_from(np.arange(self.codebook_size))
623
+
624
+ #if k < affinity_matrix.shape[1]:
625
+ if True:
626
+ for i in np.arange(self.codebook_size):
627
+ arr = affinity_matrix[i,:]
628
+ #kth_largest_value = np.partition(arr, -k)[-k]
629
+ #arr[arr<kth_largest_value] = 0
630
+ #affinity_matrix[i,:] = arr
631
+
632
+ if exclude_metacells is None:
633
+ for j in np.arange(len(arr)):
634
+ if (arr[j]>0) and (j!=i):
635
+ self.G.add_edge(i,j,weight=1/arr[j])
636
+ elif not (i in exclude_metacells):
637
+ for j in np.arange(len(arr)):
638
+ if (arr[j]>0) and (j!=i) and (not j in exclude_metacells):
639
+ self.G.add_edge(i,j,weight=1/arr[j])
640
+
641
+ return self.G
642
+
643
+ def metacell_fa2(self, G, max_iter=100):
644
+ return visualize_metacell_igraph_with_fa2(G, iterations=max_iter)
645
+
646
+ def metacell_tree(self, G, root_metacell=0):
647
+ T = nx.minimum_spanning_tree(G)
648
+ sorted(T.edges(data=True))
649
+ tree = nx.dfs_tree(T, root_metacell)
650
+
651
+ return tree
652
+
653
+ def preprocess(self, xs, threshold=0):
654
+ if self.loss_func == 'bernoulli':
655
+ ad = sc.AnnData(xs)
656
+ binarize(ad, threshold=threshold)
657
+ xs = ad.X.copy()
658
+ else:
659
+ xs = np.round(xs)
660
+
661
+ if sparse.issparse(xs):
662
+ xs = xs.toarray()
663
+ return xs
664
+
665
+ def fit(self, xs,
666
+ fss: list = None,
667
+ num_epochs: int = 100,
668
+ learning_rate: float = 0.0001,
669
+ batch_size: int = 256,
670
+ algo: Literal['adam','rmsprop','adamw'] = 'adam',
671
+ beta_1: float = 0.9,
672
+ weight_decay: float = 0.005,
673
+ decay_rate: float = 0.9,
674
+ threshold: int = 0,
675
+ normalize: bool = True,
676
+ config_enum: str = 'parallel',
677
+ use_jax: bool = False,
678
+ show_progress: bool = True,):
679
+ """
680
+ Train the SURE model.
681
+
682
+ Parameters
683
+ ----------
684
+ xs
685
+ Single-cell experssion matrix. It should be a Numpy array or a Pytorch Tensor. Rows are cells and columns are features.
686
+ us
687
+ Undesired factor matrix. It should be a Numpy array or a Pytorch Tensor. Rows are cells and columns are undesired factors.
688
+ num_epochs
689
+ Number of training epochs.
690
+ learning_rate
691
+ Parameter for training.
692
+ batch_size
693
+ Size of batch processing.
694
+ algo
695
+ Optimization algorithm.
696
+ beta_1
697
+ Parameter for optimization.
698
+ weight_decay
699
+ Parameter for optimization.
700
+ decay_rate
701
+ Parameter for optimization.
702
+ use_jax
703
+ If toggled on, Jax will be used for speeding up. CAUTION: This will raise errors because of unknown reasons when it is called in
704
+ the Python script or Jupyter notebook. It is OK if it is used when runing SURE in the shell command.
705
+ """
706
+ self.normalize = normalize
707
+
708
+ xs = self.preprocess(xs, threshold=threshold)
709
+ xs = convert_to_tensor(xs, dtype=self.dtype, device='cpu')
710
+ if fss is not None:
711
+ fs = np.hstack(fss)
712
+ fs = convert_to_tensor(fs, dtype=self.dtype, device='cpu')
713
+
714
+ dataset = CustomDataset(xs)
715
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
716
+
717
+ # setup the optimizer
718
+ optim_params = {'lr': learning_rate, 'betas': (beta_1, 0.999), 'weight_decay': weight_decay}
719
+
720
+ if algo.lower()=='rmsprop':
721
+ optimizer = torch.optim.RMSprop
722
+ elif algo.lower()=='adam':
723
+ optimizer = torch.optim.Adam
724
+ elif algo.lower() == 'adamw':
725
+ optimizer = torch.optim.AdamW
726
+ else:
727
+ raise ValueError("An optimization algorithm must be specified.")
728
+ scheduler = ExponentialLR({'optimizer': optimizer, 'optim_args': optim_params, 'gamma': decay_rate})
729
+
730
+ pyro.clear_param_store()
731
+
732
+ # set up the loss(es) for inference, wrapping the guide in config_enumerate builds the loss as a sum
733
+ # by enumerating each class label form the sampled discrete categorical distribution in the model
734
+ Elbo = JitTraceEnum_ELBO if use_jax else TraceEnum_ELBO
735
+ elbo = Elbo(max_plate_nesting=1, strict_enumeration_warning=False)
736
+ guide = config_enumerate(self.guide, config_enum, expand=True)
737
+ loss_basic = SVI(self.model, guide, scheduler, loss=elbo)
738
+
739
+ # build a list of all losses considered
740
+ losses = [loss_basic]
741
+ num_losses = len(losses)
742
+
743
+ with tqdm(total=num_epochs, desc='Training', unit='epoch', disable=not show_progress) as pbar:
744
+ for _ in range(num_epochs):
745
+ epoch_losses = [0.0] * num_losses
746
+ for batch_x, idx in dataloader:
747
+ batch_x = batch_x.to(self.get_device())
748
+ if fss is None:
749
+ batch_u = None
750
+ else:
751
+ batch_u = fs[idx].to(self.get_device())
752
+
753
+ for loss_id in range(num_losses):
754
+ new_loss = losses[loss_id].step(batch_x, batch_u)
755
+ epoch_losses[loss_id] += new_loss
756
+
757
+ avg_epoch_losses_ = map(lambda v: v / len(dataloader), epoch_losses)
758
+ avg_epoch_losses = map(lambda v: "{:.4f}".format(v), avg_epoch_losses_)
759
+
760
+ # store the loss
761
+ str_loss = " ".join(map(str, avg_epoch_losses))
762
+
763
+ # Update progress bar
764
+ pbar.set_postfix({'loss': str_loss})
765
+ pbar.update(1)
766
+
767
+ @classmethod
768
+ def save_model(cls, model, file_path, compression=False):
769
+ """Save the model to the specified file path."""
770
+ file_path = os.path.abspath(file_path)
771
+
772
+ model.eval()
773
+ if compression:
774
+ with gzip.open(file_path, 'wb') as pickle_file:
775
+ pickle.dump(model, pickle_file)
776
+ else:
777
+ with open(file_path, 'wb') as pickle_file:
778
+ pickle.dump(model, pickle_file)
779
+
780
+ print(f'Model saved to {file_path}')
781
+
782
+ @classmethod
783
+ def load_model(cls, file_path):
784
+ """Load the model from the specified file path and return an instance."""
785
+ print(f'Model loaded from {file_path}')
786
+
787
+ file_path = os.path.abspath(file_path)
788
+ if file_path.endswith('gz'):
789
+ with gzip.open(file_path, 'rb') as pickle_file:
790
+ model = pickle.load(pickle_file)
791
+ else:
792
+ with open(file_path, 'rb') as pickle_file:
793
+ model = pickle.load(pickle_file)
794
+
795
+ return model
796
+
797
+
798
+ EXAMPLE_RUN = (
799
+ "example run: DNF --help"
800
+ )
801
+
802
+ def parse_args():
803
+ parser = argparse.ArgumentParser(
804
+ description="DNF\n{}".format(EXAMPLE_RUN))
805
+
806
+ parser.add_argument(
807
+ "--cuda", action="store_true", help="use GPU(s) to speed up training"
808
+ )
809
+ parser.add_argument(
810
+ "--jit", action="store_true", help="use PyTorch jit to speed up training"
811
+ )
812
+ parser.add_argument(
813
+ "-n", "--num-epochs", default=200, type=int, help="number of epochs to run"
814
+ )
815
+ parser.add_argument(
816
+ "-enum",
817
+ "--enum-discrete",
818
+ default="parallel",
819
+ help="parallel, sequential or none. uses parallel enumeration by default",
820
+ )
821
+ parser.add_argument(
822
+ "-data",
823
+ "--data-file",
824
+ default=None,
825
+ type=str,
826
+ help="the data file",
827
+ )
828
+ parser.add_argument(
829
+ "-undesired",
830
+ "--undesired-factor-file",
831
+ default=None,
832
+ type=str,
833
+ help="the file for the record of undesired factors",
834
+ )
835
+ parser.add_argument(
836
+ "-64",
837
+ "--float64",
838
+ action="store_true",
839
+ help="use double float precision",
840
+ )
841
+ parser.add_argument(
842
+ "--z-dist",
843
+ default='gumbel',
844
+ type=str,
845
+ choices=['normal','laplacian','cauchy','studentt','gumbel'],
846
+ help="distribution model for latent representation",
847
+ )
848
+ parser.add_argument(
849
+ "-zd",
850
+ "--z-dim",
851
+ default=10,
852
+ type=int,
853
+ help="size of the tensor representing the latent variable z",
854
+ )
855
+ parser.add_argument(
856
+ "-cs",
857
+ "--codebook_size",
858
+ default=30,
859
+ type=int,
860
+ help="size of vector quantization codebook",
861
+ )
862
+ parser.add_argument(
863
+ "-dd",
864
+ "--d-dim",
865
+ default=3,
866
+ type=int,
867
+ choices=[2,3],
868
+ help="size of the vector quantization codeword",
869
+ )
870
+ parser.add_argument(
871
+ "--d-dist",
872
+ default='studentt',
873
+ type=str,
874
+ choices=['normal','laplacian','cauchy','vonmises','gumbel','studentt'],
875
+ help="distribution model for visual representation",
876
+ )
877
+ parser.add_argument(
878
+ "-hl",
879
+ "--hidden-layers",
880
+ nargs="+",
881
+ default=[300],
882
+ type=int,
883
+ help="a tuple (or list) of MLP layers to be used in the neural networks "
884
+ "representing the parameters of the distributions in our model",
885
+ )
886
+ parser.add_argument(
887
+ "-hla",
888
+ "--hidden-layer-activation",
889
+ default='relu',
890
+ type=str,
891
+ choices=['relu','softplus','leakyrelu','linear'],
892
+ help="activation function for hidden layers",
893
+ )
894
+ parser.add_argument(
895
+ "-plf",
896
+ "--post-layer-function",
897
+ nargs="+",
898
+ default=['layernorm'],
899
+ type=str,
900
+ help="post functions for hidden layers, could be none, dropout, layernorm, batchnorm, or combination, default is 'dropout layernorm'",
901
+ )
902
+ parser.add_argument(
903
+ "-paf",
904
+ "--post-activation-function",
905
+ nargs="+",
906
+ default=['none'],
907
+ type=str,
908
+ help="post functions for activation layers, could be none or dropout, default is 'none'",
909
+ )
910
+ parser.add_argument(
911
+ "-id",
912
+ "--inverse-dispersion",
913
+ default=10.0,
914
+ type=float,
915
+ help="inverse dispersion prior for negative binomial",
916
+ )
917
+ parser.add_argument(
918
+ "-lr",
919
+ "--learning-rate",
920
+ default=0.0001,
921
+ type=float,
922
+ help="learning rate for Adam optimizer",
923
+ )
924
+ parser.add_argument(
925
+ "-dr",
926
+ "--decay-rate",
927
+ default=0.9,
928
+ type=float,
929
+ help="decay rate for Adam optimizer",
930
+ )
931
+ parser.add_argument(
932
+ "--layer-dropout-rate",
933
+ default=0.1,
934
+ type=float,
935
+ help="droput rate for neural networks",
936
+ )
937
+ parser.add_argument(
938
+ "-b1",
939
+ "--beta-1",
940
+ default=0.95,
941
+ type=float,
942
+ help="beta-1 parameter for Adam optimizer",
943
+ )
944
+ parser.add_argument(
945
+ "-bs",
946
+ "--batch-size",
947
+ default=1000,
948
+ type=int,
949
+ help="number of cells to be considered in a batch",
950
+ )
951
+ parser.add_argument(
952
+ "-likeli",
953
+ "--likelihood",
954
+ default='negbinomial',
955
+ type=str,
956
+ choices=['negbinomial', 'multinomial', 'poisson'],
957
+ help="specify the distribution likelihood function",
958
+ )
959
+ parser.add_argument(
960
+ "--seed",
961
+ default=None,
962
+ type=int,
963
+ help="seed for controlling randomness in this example",
964
+ )
965
+ parser.add_argument(
966
+ "--save-model",
967
+ default=None,
968
+ type=str,
969
+ help="path to save model for prediction",
970
+ )
971
+ args = parser.parse_args()
972
+
973
+ return args
974
+
975
+ def set_random_seed(seed):
976
+ # Set seed for PyTorch
977
+ torch.manual_seed(seed)
978
+
979
+ # If using CUDA, set the seed for CUDA
980
+ if torch.cuda.is_available():
981
+ torch.cuda.manual_seed(seed)
982
+ torch.cuda.manual_seed_all(seed) # For multi-GPU setups.
983
+
984
+ # Set seed for NumPy
985
+ np.random.seed(seed)
986
+
987
+ # Set seed for Python's random module
988
+ random.seed(seed)
989
+
990
+ # Set seed for Pyro
991
+ pyro.set_rng_seed(seed)
992
+
993
+
994
+
995
+ def main():
996
+ args = parse_args()
997
+
998
+ assert (
999
+ (args.data_file is not None) and (
1000
+ os.path.exists(args.data_file))
1001
+ ), "data file must be provided"
1002
+
1003
+ if args.float64:
1004
+ dtype = torch.float64
1005
+ torch.set_default_dtype(torch.float64)
1006
+ else:
1007
+ dtype = torch.float32
1008
+ torch.set_default_dtype(torch.float32)
1009
+
1010
+ xs = dt.fread(file=args.data_file, header=True).to_numpy()
1011
+ us = None
1012
+ if args.undesired_factor_file is not None:
1013
+ us = dt.fread(file=args.undesired_factor_file, header=True).to_numpy()
1014
+
1015
+ input_size = xs.shape[1]
1016
+ covariate_size = 0 if us is None else us.shape[1]
1017
+
1018
+ z_dist = args.z_dist
1019
+ d_dist = args.d_dist
1020
+
1021
+ # batch_size: number of cells (and labels) to be considered in a batch
1022
+ DNF = DNF(
1023
+ input_size=input_size,
1024
+ covariate_size=covariate_size,
1025
+ codebook_size=args.codebook_size,
1026
+ d_dim=args.d_dim,
1027
+ d_dist=d_dist,
1028
+ z_dim=args.z_dim,
1029
+ z_dist=z_dist,
1030
+ hidden_layers=args.hidden_layers,
1031
+ hidden_layer_activation=args.hidden_layer_activation,
1032
+ loss_func=args.likelihood,
1033
+ inverse_dispersion=args.inverse_dispersion,
1034
+ nn_dropout=args.layer_dropout_rate,
1035
+ use_cuda=args.cuda,
1036
+ config_enum=args.enum_discrete,
1037
+ post_layer_fct=args.post_layer_function,
1038
+ post_act_fct=args.post_activation_function,
1039
+ dtype=dtype,
1040
+ seed=args.seed,
1041
+ )
1042
+
1043
+ DNF.fit(xs, us = us,
1044
+ num_epochs=args.num_epochs,
1045
+ learning_rate=args.learning_rate,
1046
+ batch_size=args.batch_size,
1047
+ beta_1=args.beta_1,
1048
+ decay_rate=args.decay_rate,
1049
+ use_jax=args.jit,
1050
+ config_enum=args.enum_discrete,
1051
+ )
1052
+
1053
+ if args.save_model is not None:
1054
+ DNF.save_model(DNF, args.save_model)
1055
+
1056
+
1057
+ if __name__ == "__main__":
1058
+ main()