geneflowdecoder 0.1.5__tar.gz → 0.1.7__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.
- {geneflowdecoder-0.1.5/src/geneflowdecoder.egg-info → geneflowdecoder-0.1.7}/PKG-INFO +1 -1
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/pyproject.toml +1 -1
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/src/geneflowdecoder/_core.py +182 -234
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/src/geneflowdecoder/_version.py +1 -1
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7/src/geneflowdecoder.egg-info}/PKG-INFO +1 -1
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/LICENSE +0 -0
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/README.md +0 -0
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/setup.cfg +0 -0
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/src/geneflowdecoder/__init__.py +0 -0
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/src/geneflowdecoder.egg-info/SOURCES.txt +0 -0
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/src/geneflowdecoder.egg-info/dependency_links.txt +0 -0
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/src/geneflowdecoder.egg-info/requires.txt +0 -0
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/src/geneflowdecoder.egg-info/top_level.txt +0 -0
- {geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/tests/test_api.py +0 -0
|
@@ -1,83 +1,59 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
"""
|
|
3
|
-
GeneFlowDecoder
|
|
4
|
-
z
|
|
3
|
+
GeneFlowDecoder —— z 为【固定表征】版本
|
|
4
|
+
z -> X (log1p expression)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
接口(稳定):
|
|
7
7
|
model = GeneFlowDecoder(n_genes=G, z_dim=D, ...)
|
|
8
8
|
model.fit(X, z, X_val=None, z_val=None, ...)
|
|
9
|
-
model(z) #
|
|
10
|
-
model.predict(z)
|
|
11
|
-
model.save(path) / GeneFlowDecoder.load(path, **
|
|
9
|
+
model(z) # 采样(Flow,随机)
|
|
10
|
+
model(z, deterministic=True) / model.predict(z) # NB 均值,确定性
|
|
11
|
+
model.save(path) / GeneFlowDecoder.load(path, **kw)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
依赖: torch >= 2.1, numpy
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
18
|
import math
|
|
19
19
|
import warnings
|
|
20
|
-
from
|
|
21
|
-
from typing import Any, Optional, Tuple, Union
|
|
20
|
+
from typing import Optional, Tuple, Union
|
|
22
21
|
|
|
23
22
|
import numpy as np
|
|
24
23
|
|
|
25
|
-
# ----------------------------------------------------------------------
|
|
26
|
-
# 可选的 torch 导入:缺 torch 时 import 不崩,构造模型时才明确报错
|
|
27
|
-
# ----------------------------------------------------------------------
|
|
28
24
|
try:
|
|
29
25
|
import torch
|
|
30
26
|
import torch.nn as nn
|
|
31
27
|
import torch.nn.functional as F
|
|
32
|
-
from torch import Tensor
|
|
33
|
-
|
|
34
28
|
_HAS_TORCH = True
|
|
35
29
|
except Exception: # pragma: no cover
|
|
36
30
|
_HAS_TORCH = False
|
|
37
|
-
torch = None # type: ignore[assignment]
|
|
38
|
-
nn = None # type: ignore[assignment]
|
|
39
|
-
F = None # type: ignore[assignment]
|
|
40
|
-
|
|
41
|
-
class Tensor: # noqa: N801 -- placeholder type, never instantiated
|
|
42
|
-
"""Stand-in so ``Array`` remains valid at annotation-check time."""
|
|
43
|
-
|
|
44
|
-
def __init_subclass__(cls, **_: Any) -> None:
|
|
45
|
-
pass
|
|
46
|
-
|
|
47
|
-
class _Noop:
|
|
48
|
-
def __getattr__(self, _name: str) -> "_Noop":
|
|
49
|
-
return self
|
|
50
|
-
|
|
51
|
-
def __call__(self, *__args: Any, **__kw: Any) -> None:
|
|
52
|
-
return None
|
|
53
31
|
|
|
54
|
-
|
|
55
|
-
F = _Noop() # type: ignore[assignment]
|
|
32
|
+
Array = Union["torch.Tensor", np.ndarray]
|
|
56
33
|
|
|
57
34
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _as_tensor(x: Array, device: "torch.device") -> "Tensor":
|
|
62
|
-
"""numpy -> torch; 已在目标设备的 tensor 原样返回;cuda 输入不再拉回 cpu。"""
|
|
35
|
+
def _as_tensor(x: Array, device) -> "torch.Tensor":
|
|
63
36
|
if isinstance(x, np.ndarray):
|
|
64
37
|
x = torch.from_numpy(x)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
return t
|
|
38
|
+
if _HAS_TORCH and isinstance(x, torch.Tensor):
|
|
39
|
+
return x.to(device, non_blocking=True)
|
|
40
|
+
return x
|
|
69
41
|
|
|
70
42
|
|
|
71
|
-
def
|
|
72
|
-
|
|
43
|
+
def _shape_ok(t: "torch.Tensor", ref: Tuple[int, ...], name: str) -> None:
|
|
44
|
+
"""真实前向路径的形状断言:只在形状真的错时才报,且不猜维度。"""
|
|
45
|
+
if not _HAS_TORCH or not isinstance(t, torch.Tensor):
|
|
46
|
+
return
|
|
47
|
+
if tuple(t.shape) != tuple(ref):
|
|
73
48
|
raise RuntimeError(
|
|
74
|
-
"GeneFlowDecoder
|
|
75
|
-
"
|
|
49
|
+
f"[GeneFlowDecoder] 形状不一致: {name} = {tuple(t.shape)}, "
|
|
50
|
+
f"期望 {tuple(ref)}。通常是因为 X 的列数与 n_genes 不一致,"
|
|
51
|
+
f"或上游线性层输出维数不是 1。"
|
|
76
52
|
)
|
|
77
53
|
|
|
78
54
|
|
|
79
55
|
# ============================================================
|
|
80
|
-
#
|
|
56
|
+
# AdaLN 条件化
|
|
81
57
|
# ============================================================
|
|
82
58
|
|
|
83
59
|
class _AdaLNModulation(nn.Module):
|
|
@@ -85,12 +61,12 @@ class _AdaLNModulation(nn.Module):
|
|
|
85
61
|
super().__init__()
|
|
86
62
|
self.norm = nn.LayerNorm(d, elementwise_affine=False, eps=1e-6)
|
|
87
63
|
self.proj = nn.Sequential(
|
|
88
|
-
nn.Linear(cond_dim, d), nn.SiLU(), nn.Linear(d, 3 * d)
|
|
64
|
+
nn.Linear(cond_dim, d), nn.SiLU(), nn.Linear(d, 3 * d),
|
|
89
65
|
)
|
|
90
66
|
nn.init.zeros_(self.proj[-1].weight)
|
|
91
67
|
nn.init.zeros_(self.proj[-1].bias)
|
|
92
68
|
|
|
93
|
-
def forward(self, x: Tensor, z: Tensor) -> Tensor:
|
|
69
|
+
def forward(self, x: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
|
|
94
70
|
g, b, gate = self.proj(z).chunk(3, dim=-1)
|
|
95
71
|
g = g.unsqueeze(1)
|
|
96
72
|
b = b.unsqueeze(1)
|
|
@@ -104,10 +80,10 @@ class _CondBlock(nn.Module):
|
|
|
104
80
|
self.attn_norm = _AdaLNModulation(d, cond_dim)
|
|
105
81
|
self.attn = nn.MultiheadAttention(d, n_heads, batch_first=True)
|
|
106
82
|
self.mlp_norm = _AdaLNModulation(d, cond_dim)
|
|
107
|
-
|
|
108
|
-
self.mlp = nn.Sequential(nn.Linear(d,
|
|
83
|
+
h = int(d * mlp_ratio)
|
|
84
|
+
self.mlp = nn.Sequential(nn.Linear(d, h), nn.GELU(), nn.Linear(h, d))
|
|
109
85
|
|
|
110
|
-
def forward(self, x: Tensor, z: Tensor) -> Tensor:
|
|
86
|
+
def forward(self, x: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
|
|
111
87
|
h = self.attn_norm(x, z)
|
|
112
88
|
h, _ = self.attn(h, h, h, need_weights=False)
|
|
113
89
|
x = x + h
|
|
@@ -115,36 +91,25 @@ class _CondBlock(nn.Module):
|
|
|
115
91
|
|
|
116
92
|
|
|
117
93
|
# ============================================================
|
|
118
|
-
# Gene Transformer backbone
|
|
119
|
-
# 输出恒为 [B, G, d];chunked 分支显式保证拼接后形状
|
|
94
|
+
# Gene Transformer backbone: [B, G, d] -> [B, G, d]
|
|
120
95
|
# ============================================================
|
|
121
96
|
|
|
122
97
|
class _GeneTransformer(nn.Module):
|
|
123
|
-
def __init__(
|
|
124
|
-
|
|
125
|
-
n_genes: int,
|
|
126
|
-
d: int,
|
|
127
|
-
depth: int,
|
|
128
|
-
n_heads: int,
|
|
129
|
-
cond_dim: int,
|
|
130
|
-
chunk_size: int,
|
|
131
|
-
ckpt: bool,
|
|
132
|
-
):
|
|
98
|
+
def __init__(self, n_genes: int, d: int, depth: int, n_heads: int,
|
|
99
|
+
cond_dim: int, chunk_size: int, ckpt: bool):
|
|
133
100
|
super().__init__()
|
|
134
101
|
self.d = d
|
|
135
102
|
self.n_genes = n_genes
|
|
136
103
|
self.chunk_size = max(1, chunk_size)
|
|
137
|
-
self._ckpt = ckpt
|
|
138
|
-
|
|
139
104
|
self.gene_embed = nn.Embedding(n_genes, d)
|
|
140
105
|
nn.init.normal_(self.gene_embed.weight, std=0.02)
|
|
141
106
|
self.pos_embed = nn.Parameter(torch.zeros(1, n_genes, d))
|
|
142
107
|
nn.init.normal_(self.pos_embed, std=0.02)
|
|
143
|
-
|
|
144
108
|
self.blocks = nn.ModuleList([_CondBlock(d, cond_dim, n_heads) for _ in range(depth)])
|
|
145
109
|
self.head_norm = nn.LayerNorm(d, eps=1e-6)
|
|
110
|
+
self._ckpt = ckpt
|
|
146
111
|
|
|
147
|
-
def _run(self, x: Tensor, z: Tensor) -> Tensor:
|
|
112
|
+
def _run(self, x: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
|
|
148
113
|
for blk in self.blocks:
|
|
149
114
|
if self._ckpt and self.training:
|
|
150
115
|
x = torch.utils.checkpoint.checkpoint(blk, x, z, use_reentrant=False)
|
|
@@ -152,73 +117,84 @@ class _GeneTransformer(nn.Module):
|
|
|
152
117
|
x = blk(x, z)
|
|
153
118
|
return self.head_norm(x)
|
|
154
119
|
|
|
155
|
-
def forward(self, gene_ids: Tensor, z: Tensor) -> Tensor:
|
|
156
|
-
B, G = gene_ids.shape[0], gene_ids.shape[1]
|
|
120
|
+
def forward(self, gene_ids: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
|
|
121
|
+
B, G = gene_ids.shape[0], gene_ids.shape[-1]
|
|
157
122
|
if G <= self.chunk_size:
|
|
158
123
|
h = self.gene_embed(gene_ids) + self.pos_embed[:, :G, :]
|
|
159
124
|
return self._run(h, z)
|
|
160
|
-
|
|
161
|
-
chunks: list[Tensor] = []
|
|
125
|
+
outs: list[torch.Tensor] = []
|
|
162
126
|
for s in range(0, G, self.chunk_size):
|
|
163
127
|
e = min(s + self.chunk_size, G)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
out = torch.cat(chunks, dim=1)
|
|
168
|
-
# 防御性断言:backbone 永远交出 [B, G, d]
|
|
169
|
-
if out.shape != (B, G, self.d): # pragma: no cover - 仅开发期暴露
|
|
170
|
-
raise RuntimeError(
|
|
171
|
-
f"backbone shape invariant broken: got {tuple(out.shape)}, "
|
|
172
|
-
f"expected ({B},{G},{self.d})"
|
|
173
|
-
)
|
|
174
|
-
return out
|
|
128
|
+
h = self.gene_embed(gene_ids[..., s:e]) + self.pos_embed[:, s:e, :]
|
|
129
|
+
outs.append(self._run(h, z))
|
|
130
|
+
return torch.cat(outs, dim=1)
|
|
175
131
|
|
|
176
132
|
|
|
177
133
|
# ============================================================
|
|
178
|
-
# NB
|
|
134
|
+
# 输出头:速度场 与 NB 均值,都只对【特征维 d】做线性 -> 1
|
|
179
135
|
# ============================================================
|
|
180
136
|
|
|
137
|
+
class _VelocityHead(nn.Module):
|
|
138
|
+
"""h:[B,G,d] -> v:[B,G]。out_features=1,绝不对 n_genes 做映射。"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, d: int):
|
|
141
|
+
super().__init__()
|
|
142
|
+
self.net = nn.Linear(d, 1)
|
|
143
|
+
nn.init.zeros_(self.net.weight)
|
|
144
|
+
nn.init.zeros_(self.net.bias)
|
|
145
|
+
|
|
146
|
+
def forward(self, h: torch.Tensor) -> torch.Tensor:
|
|
147
|
+
# h 可能是 [B,G,d] 或退化 [G,d]/[d]
|
|
148
|
+
if h.ndim < 2:
|
|
149
|
+
raise RuntimeError(f"_VelocityHead 输入维度异常: {tuple(h.shape)}")
|
|
150
|
+
v = self.net(h).squeeze(-1) # [B,G,1]->[B,G]
|
|
151
|
+
if v.ndim == 1: # [G] -> [1,G]
|
|
152
|
+
v = v.unsqueeze(0)
|
|
153
|
+
return v
|
|
154
|
+
|
|
155
|
+
|
|
181
156
|
class _NBHead(nn.Module):
|
|
157
|
+
"""mu 逐基因逐细胞一个标量;theta 每个基因一个 dispersion。"""
|
|
158
|
+
|
|
182
159
|
def __init__(self, d: int, n_genes: int):
|
|
183
160
|
super().__init__()
|
|
184
|
-
self.mu_proj = nn.Linear(d, n_genes
|
|
161
|
+
self.mu_proj = nn.Linear(d, 1) # <- 关键:不是 n_genes
|
|
185
162
|
self.theta = nn.Parameter(torch.zeros(n_genes))
|
|
186
163
|
nn.init.zeros_(self.mu_proj.weight)
|
|
187
164
|
nn.init.zeros_(self.mu_proj.bias)
|
|
188
165
|
|
|
189
|
-
def forward(self, h: Tensor) -> Tuple[Tensor, Tensor]:
|
|
190
|
-
|
|
191
|
-
|
|
166
|
+
def forward(self, h: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
167
|
+
log_mu = self.mu_proj(h).squeeze(-1)
|
|
168
|
+
if log_mu.ndim == 1:
|
|
169
|
+
log_mu = log_mu.unsqueeze(0)
|
|
170
|
+
return log_mu, F.softplus(self.theta) + 1e-3
|
|
192
171
|
|
|
193
172
|
@staticmethod
|
|
194
|
-
def nll(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
x: Tensor,
|
|
198
|
-
mask: Optional[Tensor] = None,
|
|
199
|
-
) -> Tensor:
|
|
173
|
+
def nll(log_mu: torch.Tensor, theta: torch.Tensor,
|
|
174
|
+
x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
|
|
175
|
+
_shape_ok(log_mu, x.shape, "log_mu")
|
|
200
176
|
mu = log_mu.exp().clamp(min=1e-6)
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
+ th * (th.log() - (th + mu).log())
|
|
207
|
-
+ x * (mu.log() - (th + mu).log())
|
|
208
|
-
)
|
|
177
|
+
# theta:[G] -> [1,G],与 [B,G] 广播;不赌 x 的 stride/layout
|
|
178
|
+
th = theta.unsqueeze(0)
|
|
179
|
+
ll = (torch.lgamma(x + th) - torch.lgamma(th) - torch.lgamma(x + 1)
|
|
180
|
+
+ th * (th.log() - (th + mu).log())
|
|
181
|
+
+ x * (mu.log() - (th + mu).log()))
|
|
209
182
|
if mask is not None:
|
|
210
183
|
ll = ll * mask
|
|
211
184
|
return -ll.sum() / mask.sum().clamp(min=1.0)
|
|
212
185
|
return -ll.mean()
|
|
213
186
|
|
|
214
187
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
188
|
+
# ============================================================
|
|
189
|
+
# 配置
|
|
190
|
+
# ============================================================
|
|
218
191
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
192
|
+
class FlowConfig:
|
|
193
|
+
def __init__(self, sigma_min: float = 1e-4,
|
|
194
|
+
nb_weight: float = 1.0, corr_weight: float = 0.1):
|
|
195
|
+
self.sigma_min = sigma_min
|
|
196
|
+
self.nb_weight = nb_weight
|
|
197
|
+
self.corr_weight = corr_weight
|
|
222
198
|
|
|
223
199
|
|
|
224
200
|
# ============================================================
|
|
@@ -227,18 +203,12 @@ class FlowConfig:
|
|
|
227
203
|
|
|
228
204
|
class GeneFlowDecoder(nn.Module):
|
|
229
205
|
"""
|
|
230
|
-
z -> X (log1p
|
|
231
|
-
|
|
232
|
-
``z`` 默认视为**固定表征**(freeze_z=True):不参与反传、不被优化器更新,
|
|
233
|
-
fit() 开头还会对其做一次 detach + clone 快照,训练中途外部 encoder
|
|
234
|
-
若继续更新也不会污染本 decoder 的输入。
|
|
206
|
+
z (固定表征) -> X (log1p 表达)
|
|
235
207
|
|
|
236
|
-
|
|
237
|
-
--------
|
|
238
|
-
>>> model = GeneFlowDecoder(n_genes=2048, z_dim=256, d=512, depth=8)
|
|
208
|
+
>>> model = GeneFlowDecoder(n_genes=2048, z_dim=256, freeze_z=True)
|
|
239
209
|
>>> model.fit(X_train, z_train, epochs=200, batch_size=64)
|
|
240
|
-
>>> X_hat = model(z_test) # 采样
|
|
241
|
-
>>> X_rec
|
|
210
|
+
>>> X_hat = model(z_test) # 采样
|
|
211
|
+
>>> X_rec = model.predict(z_test) # 确定性 NB 均值
|
|
242
212
|
"""
|
|
243
213
|
|
|
244
214
|
def __init__(
|
|
@@ -254,9 +224,12 @@ class GeneFlowDecoder(nn.Module):
|
|
|
254
224
|
freeze_z: bool = True,
|
|
255
225
|
device: str = "auto",
|
|
256
226
|
):
|
|
257
|
-
|
|
227
|
+
if not _HAS_TORCH:
|
|
228
|
+
raise RuntimeError(
|
|
229
|
+
"GeneFlowDecoder requires PyTorch (torch >= 2.1). "
|
|
230
|
+
"当前环境未找到 torch,请先 `pip install 'torch>=2.1'`。"
|
|
231
|
+
)
|
|
258
232
|
super().__init__()
|
|
259
|
-
|
|
260
233
|
if device == "auto":
|
|
261
234
|
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
262
235
|
self.device = torch.device(device)
|
|
@@ -266,31 +239,27 @@ class GeneFlowDecoder(nn.Module):
|
|
|
266
239
|
self._cfg = cfg if cfg is not None else FlowConfig()
|
|
267
240
|
|
|
268
241
|
self.backbone = _GeneTransformer(
|
|
269
|
-
self.n_genes, d, depth, n_heads, self.z_dim, chunk_size,
|
|
242
|
+
self.n_genes, d, depth, n_heads, self.z_dim, chunk_size,
|
|
243
|
+
gradient_checkpoint,
|
|
270
244
|
)
|
|
271
245
|
self.nb_head = _NBHead(d, self.n_genes)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
# 构造期自检:shape 链断在这里,而不是 136 个 batch 之后
|
|
280
|
-
with torch.no_grad():
|
|
281
|
-
_gdummy = self.vel_proj(torch.zeros(2, min(self.n_genes, 8), d))
|
|
282
|
-
if _gdummy.shape != (2, min(self.n_genes, 8)):
|
|
283
|
-
raise RuntimeError(
|
|
284
|
-
f"vel_proj 配置错误: 输出 {tuple(_gdummy.shape)}, "
|
|
285
|
-
f"期望 (2, {min(self.n_genes, 8)})。vel_proj 必须是 nn.Linear(d, 1)"
|
|
286
|
-
)
|
|
287
|
-
|
|
246
|
+
self.vel_head = _VelocityHead(d) # <- 新增独立速度头,d->1
|
|
247
|
+
self.time_mlp = nn.Sequential(
|
|
248
|
+
nn.Linear(128, d), nn.SiLU(), nn.Linear(d, d),
|
|
249
|
+
)
|
|
250
|
+
self._gene_ids = nn.Parameter(
|
|
251
|
+
torch.arange(self.n_genes), requires_grad=False,
|
|
252
|
+
)
|
|
288
253
|
self.to(self.device)
|
|
289
254
|
|
|
290
255
|
# --------------------------------------------------------
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
256
|
+
def _maybe_detach(self, z: torch.Tensor, *, snapshot: bool = False):
|
|
257
|
+
if not self.freeze_z or not isinstance(z, torch.Tensor):
|
|
258
|
+
return z
|
|
259
|
+
z = z.detach()
|
|
260
|
+
return z.clone() if snapshot else z
|
|
261
|
+
|
|
262
|
+
def _sincos(self, t: torch.Tensor) -> torch.Tensor:
|
|
294
263
|
half = 64
|
|
295
264
|
freqs = torch.exp(
|
|
296
265
|
-math.log(10000.0) * torch.arange(half, device=t.device) / (half - 1)
|
|
@@ -301,55 +270,38 @@ class GeneFlowDecoder(nn.Module):
|
|
|
301
270
|
e = F.pad(e, (0, 128 - e.shape[-1]))
|
|
302
271
|
return e
|
|
303
272
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
# --------------------------------------------------------
|
|
307
|
-
def _maybe_detach(self, z: Tensor, *, snapshot: bool = False) -> Tensor:
|
|
308
|
-
if not self.freeze_z:
|
|
309
|
-
return z
|
|
310
|
-
z = z.detach()
|
|
311
|
-
return z.clone() if snapshot else z
|
|
273
|
+
def _gene_ids_batch(self, B: int, G: int) -> torch.Tensor:
|
|
274
|
+
return self._gene_ids[:G].unsqueeze(0).expand(B, -1).contiguous()
|
|
312
275
|
|
|
313
276
|
# --------------------------------------------------------
|
|
314
|
-
# 速度场 v(x_t, t, z)
|
|
277
|
+
# 速度场 v(x_t, t, z) -> [B, G]
|
|
315
278
|
# --------------------------------------------------------
|
|
316
|
-
def _velocity(self, x_t: Tensor, t: Tensor,
|
|
279
|
+
def _velocity(self, x_t: torch.Tensor, t: torch.Tensor,
|
|
280
|
+
z: torch.Tensor) -> torch.Tensor:
|
|
317
281
|
z = self._maybe_detach(z)
|
|
318
282
|
B, G = x_t.shape
|
|
319
|
-
t_emb = self.time_mlp(self._sincos(t)).unsqueeze(1)
|
|
320
|
-
|
|
321
|
-
#
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
v = self.vel_proj(h).squeeze(-1) # [B,G,1]->[B,G]
|
|
326
|
-
|
|
327
|
-
if v.ndim != 2 or v.shape != (B, G): # pragma: no cover
|
|
328
|
-
raise RuntimeError(
|
|
329
|
-
f"_velocity shape bug: got {tuple(v.shape)}, expected ({B},{G}). "
|
|
330
|
-
f"vel_proj 必须是 nn.Linear(d, 1),请重装 >=0.1.2 版本。"
|
|
331
|
-
)
|
|
283
|
+
t_emb = self.time_mlp(self._sincos(t)).unsqueeze(1) # [B,1,d]
|
|
284
|
+
gids = self._gene_ids_batch(B, G)
|
|
285
|
+
h = self.backbone(gids, z) + t_emb # [B,G,d]
|
|
286
|
+
v = self.vel_head(h) # [B,G]
|
|
287
|
+
_shape_ok(v, x_t.shape, "_velocity")
|
|
332
288
|
return v
|
|
333
289
|
|
|
334
290
|
# --------------------------------------------------------
|
|
335
291
|
# 损失
|
|
336
292
|
# --------------------------------------------------------
|
|
337
|
-
def _loss(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
mask: Optional[Tensor] = None,
|
|
342
|
-
) -> Tensor:
|
|
343
|
-
z = self._maybe_detach(z)
|
|
344
|
-
B = x.shape[0]
|
|
293
|
+
def _loss(self, x: torch.Tensor, z: torch.Tensor,
|
|
294
|
+
mask: Optional[torch.Tensor] = None) -> torch.Tensor:
|
|
295
|
+
z = self._maybe_detach(z, snapshot=False)
|
|
296
|
+
B, G = x.shape
|
|
345
297
|
t = torch.rand(B, device=self.device).clamp(
|
|
346
|
-
self._cfg.sigma_min, 1.0 - self._cfg.sigma_min
|
|
347
|
-
)
|
|
298
|
+
self._cfg.sigma_min, 1.0 - self._cfg.sigma_min)
|
|
348
299
|
eps = torch.randn_like(x)
|
|
349
300
|
x_t = x.lerp(eps, t.unsqueeze(1))
|
|
350
301
|
|
|
351
|
-
v_pred = self._velocity(x_t, t, z)
|
|
352
|
-
v_target = eps - x
|
|
302
|
+
v_pred = self._velocity(x_t, t, z) # [B,G]
|
|
303
|
+
v_target = eps - x # [B,G]
|
|
304
|
+
_shape_ok(v_pred, v_target.shape, "flow v_pred")
|
|
353
305
|
mse = F.mse_loss(v_pred, v_target, reduction="none")
|
|
354
306
|
if mask is not None:
|
|
355
307
|
mse = mse * mask
|
|
@@ -357,37 +309,34 @@ class GeneFlowDecoder(nn.Module):
|
|
|
357
309
|
else:
|
|
358
310
|
mse = mse.mean()
|
|
359
311
|
|
|
360
|
-
x0_hat = x_t.lerp(v_pred.detach(), t.unsqueeze(1))
|
|
361
|
-
|
|
362
312
|
nb = torch.tensor(0.0, device=self.device)
|
|
363
313
|
if self._cfg.nb_weight > 0:
|
|
364
|
-
gids = self.
|
|
314
|
+
gids = self._gene_ids_batch(B, G)
|
|
365
315
|
log_mu, theta = self.nb_head(self.backbone(gids, z))
|
|
316
|
+
_shape_ok(log_mu, x.shape, "NB log_mu")
|
|
366
317
|
nb = self.nb_head.nll(log_mu, theta, x, mask)
|
|
367
318
|
|
|
368
319
|
corr = torch.tensor(0.0, device=self.device)
|
|
369
320
|
if self._cfg.corr_weight > 0 and mask is None:
|
|
321
|
+
x0_hat = x_t.lerp(v_pred.detach(), t.unsqueeze(1))
|
|
370
322
|
corr = self._corr(x0_hat, x)
|
|
371
323
|
|
|
372
324
|
return mse + self._cfg.nb_weight * nb + self._cfg.corr_weight * corr
|
|
373
325
|
|
|
374
326
|
@staticmethod
|
|
375
|
-
def _corr(p: Tensor, q: Tensor) -> Tensor:
|
|
327
|
+
def _corr(p: torch.Tensor, q: torch.Tensor) -> torch.Tensor:
|
|
376
328
|
p = p - p.mean(1, keepdim=True)
|
|
377
329
|
q = q - q.mean(1, keepdim=True)
|
|
378
330
|
d = p.norm(dim=1) * q.norm(dim=1) + 1e-8
|
|
379
|
-
return (1 - (p * q).sum(
|
|
331
|
+
return (1 - (p * q).sum(1) / d).mean()
|
|
380
332
|
|
|
381
333
|
# --------------------------------------------------------
|
|
382
334
|
# fit
|
|
383
335
|
# --------------------------------------------------------
|
|
384
336
|
@staticmethod
|
|
385
|
-
def _want_pin_memory(X: Tensor, z: Tensor) -> bool:
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
def on_gpu(a: Tensor) -> bool:
|
|
337
|
+
def _want_pin_memory(X: torch.Tensor, z: torch.Tensor) -> bool:
|
|
338
|
+
def on_gpu(a):
|
|
389
339
|
return isinstance(a, torch.Tensor) and a.device.type != "cpu"
|
|
390
|
-
|
|
391
340
|
return not (on_gpu(X) or on_gpu(z))
|
|
392
341
|
|
|
393
342
|
def fit(
|
|
@@ -406,42 +355,30 @@ class GeneFlowDecoder(nn.Module):
|
|
|
406
355
|
) -> "GeneFlowDecoder":
|
|
407
356
|
X = _as_tensor(X, self.device)
|
|
408
357
|
z = _as_tensor(z, self.device)
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if X.shape[1] != self.n_genes:
|
|
419
|
-
raise ValueError(
|
|
420
|
-
f"X 基因数 {X.shape[1]} 与 n_genes={self.n_genes} 不符;"
|
|
421
|
-
f"请用实际 X.shape[1] 构造模型"
|
|
358
|
+
z = self._maybe_detach(z, snapshot=True) # 冻结:每轮用同一份快照
|
|
359
|
+
|
|
360
|
+
if X.ndim != 2 or X.shape[1] != self.n_genes:
|
|
361
|
+
raise RuntimeError(
|
|
362
|
+
f"X 形状应为 [N, n_genes={self.n_genes}],实际 {tuple(X.shape)}"
|
|
363
|
+
)
|
|
364
|
+
if z.ndim != 2 or z.shape[1] != self.z_dim:
|
|
365
|
+
raise RuntimeError(
|
|
366
|
+
f"z 形状应为 [N, z_dim={self.z_dim}],实际 {tuple(z.shape)}"
|
|
422
367
|
)
|
|
423
368
|
|
|
424
|
-
# ★ 先算 pin_memory,再断言(0.1.1 的顺序 bug 已修正)
|
|
425
369
|
pin_memory = self._want_pin_memory(X, z)
|
|
426
|
-
assert (
|
|
427
|
-
X.device.type == "cpu" or z.device.type == "cpu" or not pin_memory
|
|
428
|
-
), "cuda 输入不应开启 pin_memory"
|
|
429
|
-
|
|
430
370
|
ds = torch.utils.data.TensorDataset(X, z)
|
|
431
371
|
dl = torch.utils.data.DataLoader(
|
|
432
|
-
ds,
|
|
433
|
-
|
|
434
|
-
shuffle=True,
|
|
435
|
-
num_workers=int(num_workers),
|
|
436
|
-
pin_memory=pin_memory,
|
|
372
|
+
ds, batch_size=int(batch_size), shuffle=True,
|
|
373
|
+
num_workers=int(num_workers), pin_memory=pin_memory,
|
|
437
374
|
)
|
|
438
375
|
|
|
439
376
|
has_val = False
|
|
440
377
|
if X_val is not None and z_val is not None:
|
|
441
378
|
Xv = _as_tensor(X_val, self.device)
|
|
442
|
-
zv = self.
|
|
443
|
-
if Xv.shape[1] != self.n_genes
|
|
444
|
-
raise
|
|
379
|
+
zv = self._as_z_val(z_val)
|
|
380
|
+
if Xv.shape[1] != self.n_genes:
|
|
381
|
+
raise RuntimeError(f"X_val 列数 {Xv.shape[1]} != n_genes {self.n_genes}")
|
|
445
382
|
has_val = True
|
|
446
383
|
|
|
447
384
|
params = [p for p in self.parameters() if p.requires_grad]
|
|
@@ -449,7 +386,7 @@ class GeneFlowDecoder(nn.Module):
|
|
|
449
386
|
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=int(epochs))
|
|
450
387
|
|
|
451
388
|
best_val = float("inf")
|
|
452
|
-
best_state: Optional[dict
|
|
389
|
+
best_state: Optional[dict] = None
|
|
453
390
|
no_improve = 0
|
|
454
391
|
|
|
455
392
|
for ep in range(1, int(epochs) + 1):
|
|
@@ -457,6 +394,7 @@ class GeneFlowDecoder(nn.Module):
|
|
|
457
394
|
tot = 0.0
|
|
458
395
|
n_seen = 0
|
|
459
396
|
for xb, zb in dl:
|
|
397
|
+
zb = self._maybe_detach(zb)
|
|
460
398
|
opt.zero_grad()
|
|
461
399
|
loss = self._loss(xb, zb)
|
|
462
400
|
loss.backward()
|
|
@@ -474,8 +412,7 @@ class GeneFlowDecoder(nn.Module):
|
|
|
474
412
|
vl = self._loss(Xv, zv).item()
|
|
475
413
|
msg += f" | val={vl:.4f}"
|
|
476
414
|
if vl < best_val - 1e-5:
|
|
477
|
-
best_val = vl
|
|
478
|
-
no_improve = 0
|
|
415
|
+
best_val, no_improve = vl, 0
|
|
479
416
|
best_state = {
|
|
480
417
|
k: v.detach().cpu().clone()
|
|
481
418
|
for k, v in self.state_dict().items()
|
|
@@ -494,6 +431,10 @@ class GeneFlowDecoder(nn.Module):
|
|
|
494
431
|
print(f"[fit] restored best val={best_val:.4f}")
|
|
495
432
|
return self
|
|
496
433
|
|
|
434
|
+
def _as_z_val(self, z: Array) -> torch.Tensor:
|
|
435
|
+
z = _as_tensor(z, self.device)
|
|
436
|
+
return self._maybe_detach(z, snapshot=True)
|
|
437
|
+
|
|
497
438
|
# --------------------------------------------------------
|
|
498
439
|
# 采样 / 确定性还原
|
|
499
440
|
# --------------------------------------------------------
|
|
@@ -504,15 +445,11 @@ class GeneFlowDecoder(nn.Module):
|
|
|
504
445
|
steps: int = 4,
|
|
505
446
|
method: str = "midpoint",
|
|
506
447
|
deterministic: bool = False,
|
|
507
|
-
) -> Tensor:
|
|
508
|
-
"""
|
|
509
|
-
给定 z 采样 X(log1p 空间)。
|
|
510
|
-
deterministic=True 等价于 predict();steps=4 通常够用。
|
|
511
|
-
"""
|
|
512
|
-
z = self._maybe_detach(_as_tensor(z, self.device))
|
|
448
|
+
) -> torch.Tensor:
|
|
513
449
|
if deterministic:
|
|
514
450
|
return self.predict(z)
|
|
515
|
-
|
|
451
|
+
z = _as_tensor(z, self.device)
|
|
452
|
+
z = self._maybe_detach(z)
|
|
516
453
|
self.eval()
|
|
517
454
|
B = z.shape[0]
|
|
518
455
|
G = self.n_genes
|
|
@@ -530,29 +467,40 @@ class GeneFlowDecoder(nn.Module):
|
|
|
530
467
|
return x
|
|
531
468
|
|
|
532
469
|
@torch.no_grad()
|
|
533
|
-
def predict(self, z: Array) -> Tensor:
|
|
534
|
-
"""确定性还原:NB 均值(log1p
|
|
535
|
-
z =
|
|
470
|
+
def predict(self, z: Array) -> torch.Tensor:
|
|
471
|
+
"""确定性还原:NB 均值(log1p 空间)。"""
|
|
472
|
+
z = _as_tensor(z, self.device)
|
|
473
|
+
z = self._maybe_detach(z)
|
|
536
474
|
self.eval()
|
|
537
475
|
B = z.shape[0]
|
|
538
|
-
gids = self.
|
|
539
|
-
log_mu, _ = self.nb_head(self.backbone(gids, z))
|
|
476
|
+
gids = self._gene_ids_batch(B, self.n_genes)
|
|
477
|
+
log_mu, _ = self.nb_head(self.backbone(gids, z)) # [B,G]
|
|
478
|
+
_shape_ok(log_mu, (B, self.n_genes), "predict")
|
|
540
479
|
return log_mu
|
|
541
480
|
|
|
542
481
|
# --------------------------------------------------------
|
|
543
|
-
#
|
|
482
|
+
# 序列化
|
|
544
483
|
# --------------------------------------------------------
|
|
545
484
|
def save(self, path: str) -> None:
|
|
546
|
-
torch.save(
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
485
|
+
torch.save({
|
|
486
|
+
"state": self.state_dict(),
|
|
487
|
+
"cfg": {
|
|
488
|
+
"sigma_min": self._cfg.sigma_min,
|
|
489
|
+
"nb_weight": self._cfg.nb_weight,
|
|
490
|
+
"corr_weight": self._cfg.corr_weight,
|
|
491
|
+
},
|
|
492
|
+
"freeze_z": self.freeze_z,
|
|
493
|
+
"n_genes": self.n_genes,
|
|
494
|
+
"z_dim": self.z_dim,
|
|
495
|
+
}, path)
|
|
550
496
|
|
|
551
497
|
@classmethod
|
|
552
|
-
def load(cls, path: str, **init_kwargs
|
|
498
|
+
def load(cls, path: str, **init_kwargs) -> "GeneFlowDecoder":
|
|
553
499
|
ckpt = torch.load(path, map_location="cpu")
|
|
554
|
-
init_kwargs.setdefault("
|
|
555
|
-
|
|
556
|
-
|
|
500
|
+
init_kwargs.setdefault("n_genes", ckpt.get("n_genes"))
|
|
501
|
+
init_kwargs.setdefault("z_dim", ckpt.get("z_dim"))
|
|
502
|
+
init_kwargs.setdefault("freeze_z", ckpt.get("freeze_z", True))
|
|
503
|
+
cfg = FlowConfig(**(ckpt.get("cfg") or {}))
|
|
504
|
+
model = cls(cfg=cfg, **init_kwargs)
|
|
557
505
|
model.load_state_dict(ckpt["state"])
|
|
558
506
|
return model
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
__version__ = "0.1.
|
|
2
|
+
__version__ = "0.1.7"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{geneflowdecoder-0.1.5 → geneflowdecoder-0.1.7}/src/geneflowdecoder.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|