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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geneflowdecoder
3
- Version: 0.1.6
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.6"
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,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, 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
-
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
- 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:
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
- # z 隔离:freeze_z 时 detach;fit 入口额外 clone 成快照
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) -> [B, G]
277
+ # 速度场 v(x_t, t, z) -> [B, G]
306
278
  # --------------------------------------------------------
307
- 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:
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) # [B,1,d]
311
-
312
- # 明确 [B, G] 的 gene id 网格,避免广播维歧义
313
- gids = self._gene_ids[:G].unsqueeze(0).expand(B, -1)
314
- h = self.backbone(gids, z) + t_emb # [B,G,d]
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
- self,
330
- x: Tensor,
331
- z: Tensor,
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) # [B,G]
343
- 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")
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._gene_ids[: x.shape[1]].unsqueeze(0).expand(B, -1)
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(dim=1) / d).mean()
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
- """pin_memory 只对 CPU tensor 有意义;cuda 输入直接关掉。"""
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
- # ★ 快照:freeze_z 时拷出一份独立副本,训练期与上游 z 彻底解耦
401
- z = self._maybe_detach(z, snapshot=True)
402
-
403
- if X.ndim != 2 or z.ndim != 2:
404
- raise ValueError(f"X/z 必须是 2D: got {X.ndim}D / {z.ndim}D")
405
- if X.shape[0] != z.shape[0]:
406
- raise ValueError(f"X 与 z 样本数不一致: {X.shape[0]} vs {z.shape[0]}")
407
- if z.shape[1] != self.z_dim:
408
- raise ValueError(f"z 特征维 {z.shape[1]} 与 z_dim={self.z_dim} 不符")
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
- batch_size=int(batch_size),
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._maybe_detach(_as_tensor(z_val, self.device), snapshot=True)
434
- if Xv.shape[1] != self.n_genes or zv.shape[1] != self.z_dim:
435
- 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}")
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[str, Tensor]] = None
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 空间)。伪计数用 (exp(log_mu)-1).clamp(min=0)。"""
526
- 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)
527
474
  self.eval()
528
475
  B = z.shape[0]
529
- gids = self._gene_ids[:self.n_genes].unsqueeze(0).expand(B, -1)
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
- {"state": self.state_dict(), "cfg": self._cfg, "freeze_z": self.freeze_z},
539
- path,
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: Any) -> "GeneFlowDecoder":
498
+ def load(cls, path: str, **init_kwargs) -> "GeneFlowDecoder":
544
499
  ckpt = torch.load(path, map_location="cpu")
545
- init_kwargs.setdefault("freeze_z", bool(ckpt.get("freeze_z", True)))
546
- model = cls(**init_kwargs)
547
- 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)
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.6"
2
+ __version__ = "0.1.7"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geneflowdecoder
3
- Version: 0.1.6
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