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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geneflowdecoder
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Gene Transformer + Conditional Rectified Flow decoder from cell-state z to log1p expression X.
5
5
  Author: GeneFlowDecoder Contributors
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "geneflowdecoder"
3
- version = "0.1.5"
3
+ version = "0.1.7"
4
4
  description = "Gene Transformer + Conditional Rectified Flow decoder from cell-state z to log1p expression X."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -1,83 +1,59 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """
3
- GeneFlowDecoder 0.1.2
4
- z (cell state, *frozen*) --> X (log-normalized expression, log1p space)
3
+ GeneFlowDecoder —— z 为【固定表征】版本
4
+ z -> X (log1p expression)
5
5
 
6
- Public API (stable):
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) # sample X from z (flow, stochastic)
10
- model.predict(z) # deterministic X (NB mean, log1p space)
11
- model.save(path) / GeneFlowDecoder.load(path, **init_kwargs)
9
+ model(z) # 采样(Flow,随机)
10
+ model(z, deterministic=True) / model.predict(z) # NB 均值,确定性
11
+ model.save(path) / GeneFlowDecoder.load(path, **kw)
12
12
 
13
- Requires: torch >= 2.1
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 dataclasses import dataclass, field
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
- nn = _Noop() # type: ignore[assignment]
55
- F = _Noop() # type: ignore[assignment]
32
+ Array = Union["torch.Tensor", np.ndarray]
56
33
 
57
34
 
58
- Array = Union[np.ndarray, "Tensor"]
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
- t: Tensor = x
66
- if t.device != device:
67
- t = t.to(device, non_blocking=True)
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 _require_torch() -> None:
72
- if not _HAS_TORCH:
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 requires PyTorch (>= 2.1). "
75
- "Install it with: pip install 'torch>=2.1'"
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
- hd = int(d * mlp_ratio)
108
- self.mlp = nn.Sequential(nn.Linear(d, hd), nn.GELU(), nn.Linear(hd, 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
- self,
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
- gids = gene_ids[:, s:e]
165
- h = self.gene_embed(gids) + self.pos_embed[:, s:e, :]
166
- chunks.append(self._run(h, z))
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
- # h: [B, G, d] -> log_mu: [B, G]
191
- return self.mu_proj(h).squeeze(-1), F.softplus(self.theta) + 1e-3
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
- log_mu: Tensor,
196
- theta: Tensor,
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
- th = theta.unsqueeze(0).expand_as(x)
202
- ll = (
203
- torch.lgamma(x + th)
204
- - torch.lgamma(th)
205
- - torch.lgamma(x + 1)
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
- @dataclass
216
- class FlowConfig:
217
- """训练目标权重;均为非负标量。"""
188
+ # ============================================================
189
+ # 配置
190
+ # ============================================================
218
191
 
219
- sigma_min: float = 1e-4
220
- nb_weight: float = 1.0
221
- corr_weight: float = 0.1
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 expression).
231
-
232
- ``z`` 默认视为**固定表征**(freeze_z=True):不参与反传、不被优化器更新,
233
- fit() 开头还会对其做一次 detach + clone 快照,训练中途外部 encoder
234
- 若继续更新也不会污染本 decoder 的输入。
206
+ z (固定表征) -> X (log1p 表达)
235
207
 
236
- Examples
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) # 采样 (flow, stochastic)
241
- >>> X_rec = model.predict(z_test) # 确定性还原: NB mean (log1p 空间)
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
- _require_torch()
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, gradient_checkpoint
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
- # ★ 关键:每个 (cell, gene) 位置预测一个标量速度,out_features 必须是 1
274
- self.vel_proj = nn.Linear(d, 1)
275
- self.time_mlp = nn.Sequential(nn.Linear(128, d), nn.SiLU(), nn.Linear(d, d))
276
-
277
- self._gene_ids = nn.Parameter(torch.arange(self.n_genes), requires_grad=False)
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
- def _sincos(self, t: Tensor) -> Tensor:
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
- # z 隔离:freeze_z 时 detach;fit 入口额外 clone 成快照
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) -> [B, G]
277
+ # 速度场 v(x_t, t, z) -> [B, G]
315
278
  # --------------------------------------------------------
316
- def _velocity(self, x_t: Tensor, t: Tensor, z: Tensor) -> 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) # [B,1,d]
320
-
321
- # 明确 [B, G] 的 gene id 网格,避免广播维歧义
322
- gids = self._gene_ids[:G].unsqueeze(0).expand(B, -1)
323
- h = self.backbone(gids, z) + t_emb # [B,G,d]
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
- self,
339
- x: Tensor,
340
- z: Tensor,
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) # [B,G]
352
- v_target = eps - x # [B,G],rectified-flow 目标
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._gene_ids[: x.shape[1]].unsqueeze(0).expand(B, -1)
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(dim=1) / d).mean()
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
- """pin_memory 只对 CPU tensor 有意义;cuda 输入直接关掉。"""
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
- # ★ 快照:freeze_z 时拷出一份独立副本,训练期与上游 z 彻底解耦
410
- z = self._maybe_detach(z, snapshot=True)
411
-
412
- if X.ndim != 2 or z.ndim != 2:
413
- raise ValueError(f"X/z 必须是 2D: got {X.ndim}D / {z.ndim}D")
414
- if X.shape[0] != z.shape[0]:
415
- raise ValueError(f"X 与 z 样本数不一致: {X.shape[0]} vs {z.shape[0]}")
416
- if z.shape[1] != self.z_dim:
417
- raise ValueError(f"z 特征维 {z.shape[1]} 与 z_dim={self.z_dim} 不符")
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
- batch_size=int(batch_size),
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._maybe_detach(_as_tensor(z_val, self.device), snapshot=True)
443
- if Xv.shape[1] != self.n_genes or zv.shape[1] != self.z_dim:
444
- raise ValueError("验证集维度与训练集不一致")
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[str, Tensor]] = None
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 空间)。伪计数用 (exp(log_mu)-1).clamp(min=0)。"""
535
- z = self._maybe_detach(_as_tensor(z, self.device))
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._gene_ids[:self.n_genes].unsqueeze(0).expand(B, -1)
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
- {"state": self.state_dict(), "cfg": self._cfg, "freeze_z": self.freeze_z},
548
- path,
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: Any) -> "GeneFlowDecoder":
498
+ def load(cls, path: str, **init_kwargs) -> "GeneFlowDecoder":
553
499
  ckpt = torch.load(path, map_location="cpu")
554
- init_kwargs.setdefault("freeze_z", bool(ckpt.get("freeze_z", True)))
555
- model = cls(**init_kwargs)
556
- model._cfg = ckpt.get("cfg", model._cfg)
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.5"
2
+ __version__ = "0.1.7"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geneflowdecoder
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Gene Transformer + Conditional Rectified Flow decoder from cell-state z to log1p expression X.
5
5
  Author: GeneFlowDecoder Contributors
6
6
  License: MIT
File without changes