geneflowdecoder 0.1.6__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.6/src/geneflowdecoder.egg-info → geneflowdecoder-0.1.7}/PKG-INFO +1 -1
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7}/pyproject.toml +1 -1
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7}/src/geneflowdecoder/_core.py +182 -225
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7}/src/geneflowdecoder/_version.py +1 -1
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7/src/geneflowdecoder.egg-info}/PKG-INFO +1 -1
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7}/LICENSE +0 -0
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7}/README.md +0 -0
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7}/setup.cfg +0 -0
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7}/src/geneflowdecoder/__init__.py +0 -0
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7}/src/geneflowdecoder.egg-info/SOURCES.txt +0 -0
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7}/src/geneflowdecoder.egg-info/dependency_links.txt +0 -0
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7}/src/geneflowdecoder.egg-info/requires.txt +0 -0
- {geneflowdecoder-0.1.6 → geneflowdecoder-0.1.7}/src/geneflowdecoder.egg-info/top_level.txt +0 -0
- {geneflowdecoder-0.1.6 → 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,22 +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
|
-
|
|
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
|
+
)
|
|
279
253
|
self.to(self.device)
|
|
280
254
|
|
|
281
255
|
# --------------------------------------------------------
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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:
|
|
285
263
|
half = 64
|
|
286
264
|
freqs = torch.exp(
|
|
287
265
|
-math.log(10000.0) * torch.arange(half, device=t.device) / (half - 1)
|
|
@@ -292,55 +270,38 @@ class GeneFlowDecoder(nn.Module):
|
|
|
292
270
|
e = F.pad(e, (0, 128 - e.shape[-1]))
|
|
293
271
|
return e
|
|
294
272
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
# --------------------------------------------------------
|
|
298
|
-
def _maybe_detach(self, z: Tensor, *, snapshot: bool = False) -> Tensor:
|
|
299
|
-
if not self.freeze_z:
|
|
300
|
-
return z
|
|
301
|
-
z = z.detach()
|
|
302
|
-
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()
|
|
303
275
|
|
|
304
276
|
# --------------------------------------------------------
|
|
305
|
-
# 速度场 v(x_t, t, z)
|
|
277
|
+
# 速度场 v(x_t, t, z) -> [B, G]
|
|
306
278
|
# --------------------------------------------------------
|
|
307
|
-
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:
|
|
308
281
|
z = self._maybe_detach(z)
|
|
309
282
|
B, G = x_t.shape
|
|
310
|
-
t_emb = self.time_mlp(self._sincos(t)).unsqueeze(1)
|
|
311
|
-
|
|
312
|
-
#
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
v = self.vel_proj(h).squeeze(-1) # [B,G,1]->[B,G]
|
|
317
|
-
|
|
318
|
-
if v.ndim != 2 or v.shape != (B, G): # pragma: no cover
|
|
319
|
-
raise RuntimeError(
|
|
320
|
-
f"_velocity shape bug: got {tuple(v.shape)}, expected ({B},{G}). "
|
|
321
|
-
f"vel_proj 必须是 nn.Linear(d, 1),请重装 >=0.1.2 版本。"
|
|
322
|
-
)
|
|
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")
|
|
323
288
|
return v
|
|
324
289
|
|
|
325
290
|
# --------------------------------------------------------
|
|
326
291
|
# 损失
|
|
327
292
|
# --------------------------------------------------------
|
|
328
|
-
def _loss(
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
mask: Optional[Tensor] = None,
|
|
333
|
-
) -> Tensor:
|
|
334
|
-
z = self._maybe_detach(z)
|
|
335
|
-
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
|
|
336
297
|
t = torch.rand(B, device=self.device).clamp(
|
|
337
|
-
self._cfg.sigma_min, 1.0 - self._cfg.sigma_min
|
|
338
|
-
)
|
|
298
|
+
self._cfg.sigma_min, 1.0 - self._cfg.sigma_min)
|
|
339
299
|
eps = torch.randn_like(x)
|
|
340
300
|
x_t = x.lerp(eps, t.unsqueeze(1))
|
|
341
301
|
|
|
342
|
-
v_pred = self._velocity(x_t, t, z)
|
|
343
|
-
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")
|
|
344
305
|
mse = F.mse_loss(v_pred, v_target, reduction="none")
|
|
345
306
|
if mask is not None:
|
|
346
307
|
mse = mse * mask
|
|
@@ -348,37 +309,34 @@ class GeneFlowDecoder(nn.Module):
|
|
|
348
309
|
else:
|
|
349
310
|
mse = mse.mean()
|
|
350
311
|
|
|
351
|
-
x0_hat = x_t.lerp(v_pred.detach(), t.unsqueeze(1))
|
|
352
|
-
|
|
353
312
|
nb = torch.tensor(0.0, device=self.device)
|
|
354
313
|
if self._cfg.nb_weight > 0:
|
|
355
|
-
gids = self.
|
|
314
|
+
gids = self._gene_ids_batch(B, G)
|
|
356
315
|
log_mu, theta = self.nb_head(self.backbone(gids, z))
|
|
316
|
+
_shape_ok(log_mu, x.shape, "NB log_mu")
|
|
357
317
|
nb = self.nb_head.nll(log_mu, theta, x, mask)
|
|
358
318
|
|
|
359
319
|
corr = torch.tensor(0.0, device=self.device)
|
|
360
320
|
if self._cfg.corr_weight > 0 and mask is None:
|
|
321
|
+
x0_hat = x_t.lerp(v_pred.detach(), t.unsqueeze(1))
|
|
361
322
|
corr = self._corr(x0_hat, x)
|
|
362
323
|
|
|
363
324
|
return mse + self._cfg.nb_weight * nb + self._cfg.corr_weight * corr
|
|
364
325
|
|
|
365
326
|
@staticmethod
|
|
366
|
-
def _corr(p: Tensor, q: Tensor) -> Tensor:
|
|
327
|
+
def _corr(p: torch.Tensor, q: torch.Tensor) -> torch.Tensor:
|
|
367
328
|
p = p - p.mean(1, keepdim=True)
|
|
368
329
|
q = q - q.mean(1, keepdim=True)
|
|
369
330
|
d = p.norm(dim=1) * q.norm(dim=1) + 1e-8
|
|
370
|
-
return (1 - (p * q).sum(
|
|
331
|
+
return (1 - (p * q).sum(1) / d).mean()
|
|
371
332
|
|
|
372
333
|
# --------------------------------------------------------
|
|
373
334
|
# fit
|
|
374
335
|
# --------------------------------------------------------
|
|
375
336
|
@staticmethod
|
|
376
|
-
def _want_pin_memory(X: Tensor, z: Tensor) -> bool:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
def on_gpu(a: Tensor) -> bool:
|
|
337
|
+
def _want_pin_memory(X: torch.Tensor, z: torch.Tensor) -> bool:
|
|
338
|
+
def on_gpu(a):
|
|
380
339
|
return isinstance(a, torch.Tensor) and a.device.type != "cpu"
|
|
381
|
-
|
|
382
340
|
return not (on_gpu(X) or on_gpu(z))
|
|
383
341
|
|
|
384
342
|
def fit(
|
|
@@ -397,42 +355,30 @@ class GeneFlowDecoder(nn.Module):
|
|
|
397
355
|
) -> "GeneFlowDecoder":
|
|
398
356
|
X = _as_tensor(X, self.device)
|
|
399
357
|
z = _as_tensor(z, self.device)
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
if X.shape[1] != self.n_genes:
|
|
410
|
-
raise ValueError(
|
|
411
|
-
f"X 基因数 {X.shape[1]} 与 n_genes={self.n_genes} 不符;"
|
|
412
|
-
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)}"
|
|
413
367
|
)
|
|
414
368
|
|
|
415
|
-
# ★ 先算 pin_memory,再断言(0.1.1 的顺序 bug 已修正)
|
|
416
369
|
pin_memory = self._want_pin_memory(X, z)
|
|
417
|
-
assert (
|
|
418
|
-
X.device.type == "cpu" or z.device.type == "cpu" or not pin_memory
|
|
419
|
-
), "cuda 输入不应开启 pin_memory"
|
|
420
|
-
|
|
421
370
|
ds = torch.utils.data.TensorDataset(X, z)
|
|
422
371
|
dl = torch.utils.data.DataLoader(
|
|
423
|
-
ds,
|
|
424
|
-
|
|
425
|
-
shuffle=True,
|
|
426
|
-
num_workers=int(num_workers),
|
|
427
|
-
pin_memory=pin_memory,
|
|
372
|
+
ds, batch_size=int(batch_size), shuffle=True,
|
|
373
|
+
num_workers=int(num_workers), pin_memory=pin_memory,
|
|
428
374
|
)
|
|
429
375
|
|
|
430
376
|
has_val = False
|
|
431
377
|
if X_val is not None and z_val is not None:
|
|
432
378
|
Xv = _as_tensor(X_val, self.device)
|
|
433
|
-
zv = self.
|
|
434
|
-
if Xv.shape[1] != self.n_genes
|
|
435
|
-
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}")
|
|
436
382
|
has_val = True
|
|
437
383
|
|
|
438
384
|
params = [p for p in self.parameters() if p.requires_grad]
|
|
@@ -440,7 +386,7 @@ class GeneFlowDecoder(nn.Module):
|
|
|
440
386
|
sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=int(epochs))
|
|
441
387
|
|
|
442
388
|
best_val = float("inf")
|
|
443
|
-
best_state: Optional[dict
|
|
389
|
+
best_state: Optional[dict] = None
|
|
444
390
|
no_improve = 0
|
|
445
391
|
|
|
446
392
|
for ep in range(1, int(epochs) + 1):
|
|
@@ -448,6 +394,7 @@ class GeneFlowDecoder(nn.Module):
|
|
|
448
394
|
tot = 0.0
|
|
449
395
|
n_seen = 0
|
|
450
396
|
for xb, zb in dl:
|
|
397
|
+
zb = self._maybe_detach(zb)
|
|
451
398
|
opt.zero_grad()
|
|
452
399
|
loss = self._loss(xb, zb)
|
|
453
400
|
loss.backward()
|
|
@@ -465,8 +412,7 @@ class GeneFlowDecoder(nn.Module):
|
|
|
465
412
|
vl = self._loss(Xv, zv).item()
|
|
466
413
|
msg += f" | val={vl:.4f}"
|
|
467
414
|
if vl < best_val - 1e-5:
|
|
468
|
-
best_val = vl
|
|
469
|
-
no_improve = 0
|
|
415
|
+
best_val, no_improve = vl, 0
|
|
470
416
|
best_state = {
|
|
471
417
|
k: v.detach().cpu().clone()
|
|
472
418
|
for k, v in self.state_dict().items()
|
|
@@ -485,6 +431,10 @@ class GeneFlowDecoder(nn.Module):
|
|
|
485
431
|
print(f"[fit] restored best val={best_val:.4f}")
|
|
486
432
|
return self
|
|
487
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
|
+
|
|
488
438
|
# --------------------------------------------------------
|
|
489
439
|
# 采样 / 确定性还原
|
|
490
440
|
# --------------------------------------------------------
|
|
@@ -495,15 +445,11 @@ class GeneFlowDecoder(nn.Module):
|
|
|
495
445
|
steps: int = 4,
|
|
496
446
|
method: str = "midpoint",
|
|
497
447
|
deterministic: bool = False,
|
|
498
|
-
) -> Tensor:
|
|
499
|
-
"""
|
|
500
|
-
给定 z 采样 X(log1p 空间)。
|
|
501
|
-
deterministic=True 等价于 predict();steps=4 通常够用。
|
|
502
|
-
"""
|
|
503
|
-
z = self._maybe_detach(_as_tensor(z, self.device))
|
|
448
|
+
) -> torch.Tensor:
|
|
504
449
|
if deterministic:
|
|
505
450
|
return self.predict(z)
|
|
506
|
-
|
|
451
|
+
z = _as_tensor(z, self.device)
|
|
452
|
+
z = self._maybe_detach(z)
|
|
507
453
|
self.eval()
|
|
508
454
|
B = z.shape[0]
|
|
509
455
|
G = self.n_genes
|
|
@@ -521,29 +467,40 @@ class GeneFlowDecoder(nn.Module):
|
|
|
521
467
|
return x
|
|
522
468
|
|
|
523
469
|
@torch.no_grad()
|
|
524
|
-
def predict(self, z: Array) -> Tensor:
|
|
525
|
-
"""确定性还原:NB 均值(log1p
|
|
526
|
-
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)
|
|
527
474
|
self.eval()
|
|
528
475
|
B = z.shape[0]
|
|
529
|
-
gids = self.
|
|
530
|
-
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")
|
|
531
479
|
return log_mu
|
|
532
480
|
|
|
533
481
|
# --------------------------------------------------------
|
|
534
|
-
#
|
|
482
|
+
# 序列化
|
|
535
483
|
# --------------------------------------------------------
|
|
536
484
|
def save(self, path: str) -> None:
|
|
537
|
-
torch.save(
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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)
|
|
541
496
|
|
|
542
497
|
@classmethod
|
|
543
|
-
def load(cls, path: str, **init_kwargs
|
|
498
|
+
def load(cls, path: str, **init_kwargs) -> "GeneFlowDecoder":
|
|
544
499
|
ckpt = torch.load(path, map_location="cpu")
|
|
545
|
-
init_kwargs.setdefault("
|
|
546
|
-
|
|
547
|
-
|
|
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)
|
|
548
505
|
model.load_state_dict(ckpt["state"])
|
|
549
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.6 → geneflowdecoder-0.1.7}/src/geneflowdecoder.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|