geneflowdecoder 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geneflowdecoder-0.1.0/LICENSE +21 -0
- geneflowdecoder-0.1.0/PKG-INFO +154 -0
- geneflowdecoder-0.1.0/README.md +120 -0
- geneflowdecoder-0.1.0/pyproject.toml +64 -0
- geneflowdecoder-0.1.0/setup.cfg +4 -0
- geneflowdecoder-0.1.0/src/geneflowdecoder/__init__.py +13 -0
- geneflowdecoder-0.1.0/src/geneflowdecoder/_core.py +567 -0
- geneflowdecoder-0.1.0/src/geneflowdecoder/_version.py +2 -0
- geneflowdecoder-0.1.0/src/geneflowdecoder.egg-info/PKG-INFO +154 -0
- geneflowdecoder-0.1.0/src/geneflowdecoder.egg-info/SOURCES.txt +12 -0
- geneflowdecoder-0.1.0/src/geneflowdecoder.egg-info/dependency_links.txt +1 -0
- geneflowdecoder-0.1.0/src/geneflowdecoder.egg-info/requires.txt +10 -0
- geneflowdecoder-0.1.0/src/geneflowdecoder.egg-info/top_level.txt +1 -0
- geneflowdecoder-0.1.0/tests/test_api.py +96 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GeneFlowDecoder Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geneflowdecoder
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Gene Transformer + Conditional Rectified Flow decoder from cell-state z to log1p expression X.
|
|
5
|
+
Author: GeneFlowDecoder Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ZengFLab/geneflowdecoder
|
|
8
|
+
Project-URL: Documentation, https://github.com/ZengFLab/geneflowdecoder#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/ZengFLab/geneflowdecoder
|
|
10
|
+
Project-URL: Issues, https://github.com/ZengFLab/geneflowdecoder/issues
|
|
11
|
+
Keywords: single-cell,transcriptomics,decoder,flow-matching,gene-transformer,scvi,representation-learning
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: torch>=2.1
|
|
26
|
+
Requires-Dist: numpy>=1.24
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
29
|
+
Requires-Dist: build; extra == "dev"
|
|
30
|
+
Requires-Dist: twine; extra == "dev"
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# GeneFlowDecoder
|
|
36
|
+
|
|
37
|
+
`z → X` 的细胞状态解码器:**Gene Transformer + Conditional Rectified Flow (CFM)**,
|
|
38
|
+
把固定的细胞表征 `z` 解码成 log1p 归一化表达谱 `X`。
|
|
39
|
+
|
|
40
|
+
> 设计目标:接口极简、可复现、单卡可跑 2 万+ 基因面板;`z` 默认冻结,
|
|
41
|
+
> 不参与反传,也不被优化器更新。
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from geneflowdecoder import GeneFlowDecoder
|
|
45
|
+
|
|
46
|
+
model = GeneFlowDecoder(n_genes=2048, z_dim=256, d=512, depth=8)
|
|
47
|
+
|
|
48
|
+
model.fit(X_train, z_train, # X: [N, G] log1p; z: [N, D]
|
|
49
|
+
X_val=X_val, z_val=z_val, # 可选,开启早停
|
|
50
|
+
epochs=200, batch_size=64)
|
|
51
|
+
|
|
52
|
+
X_hat = model(z_test) # 采样(生成式,log1p 空间)
|
|
53
|
+
X_hat = model(z_test, steps=8, deterministic=False)
|
|
54
|
+
|
|
55
|
+
X_rec = model.predict(z_test) # 确定性还原:NB 均值
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 安装
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# 从源码(开发模式)
|
|
64
|
+
git clone https://github.com/your-org/geneflowdecoder.git
|
|
65
|
+
cd geneflowdecoder
|
|
66
|
+
pip install -e .[dev]
|
|
67
|
+
|
|
68
|
+
# 或构建 wheel 后安装
|
|
69
|
+
python -m build
|
|
70
|
+
pip install dist/geneflowdecoder-0.1.0-py3-none-any.whl
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
依赖:`torch >= 2.1`、`numpy >= 1.24`。没有 GPU 也能在 CPU 上跑(只是慢)。
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
### `GeneFlowDecoder(...)`
|
|
80
|
+
|
|
81
|
+
| 参数 | 默认 | 说明 |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `n_genes` | — | 基因数,即输出维度 `G` |
|
|
84
|
+
| `z_dim` | — | 细胞状态 `z` 的维度 |
|
|
85
|
+
| `d` | 768 | Transformer 宽度 |
|
|
86
|
+
| `depth` | 16 | Transformer 层数 |
|
|
87
|
+
| `n_heads` | 12 | 注意力头数 |
|
|
88
|
+
| `chunk_size` | 2048 | 基因轴分块,控制显存峰值 |
|
|
89
|
+
| `gradient_checkpoint` | `True` | 激活检查点,省显存 |
|
|
90
|
+
| `cfg` | `FlowConfig()` | flow / loss 超参 |
|
|
91
|
+
| `freeze_z` | `True` | **`z` 全程 `detach`,只读条件** |
|
|
92
|
+
| `device` | `"auto"` | `"auto"` / `"cuda"` / `"cpu"` |
|
|
93
|
+
|
|
94
|
+
### `model.fit(X, z, *, X_val=None, z_val=None, epochs=200, batch_size=64, lr=2e-4, weight_decay=0.02, patience=20, num_workers=0, verbose=1) -> self`
|
|
95
|
+
|
|
96
|
+
- `X` / `X_val`:`[N, G]` log1p 表达,numpy 或 torch 都行
|
|
97
|
+
- `z` / `z_val`:`[N, z_dim]`;`freeze_z=True` 时绝不会被修改或反传
|
|
98
|
+
- 给了验证集 → 早停 + 自动回滚最优权重
|
|
99
|
+
|
|
100
|
+
### `model(z, *, steps=4, method="midpoint", deterministic=False) -> Tensor`
|
|
101
|
+
|
|
102
|
+
Flow 采样,`steps=4` 通常够用。
|
|
103
|
+
|
|
104
|
+
### `model.predict(z) -> Tensor`
|
|
105
|
+
|
|
106
|
+
确定性输出 `E[X]` 的 log1p(NB mean)。转伪计数:`(log_mu.exp() - 1).clamp(min=0)`。
|
|
107
|
+
|
|
108
|
+
### `model.save(path)` / `GeneFlowDecoder.load(path, device="auto")`
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## `FlowConfig`
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from geneflowdecoder import FlowConfig
|
|
116
|
+
|
|
117
|
+
cfg = FlowConfig(
|
|
118
|
+
sigma_min=1e-4, # t 的截断,防 t≈0 数值爆炸
|
|
119
|
+
nb_weight=1.0, # NB 似然辅助损失权重
|
|
120
|
+
corr_weight=0.1, # cell-wise Pearson 相关性正则
|
|
121
|
+
)
|
|
122
|
+
model = GeneFlowDecoder(n_genes=G, z_dim=D, cfg=cfg)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 发布
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python -m build # 生成 sdist + wheel 到 dist/
|
|
131
|
+
twine check dist/* # 校验元数据
|
|
132
|
+
twine upload dist/* # 发布到 PyPI(需账号 token)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 目录结构
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
geneflowdecoder/
|
|
141
|
+
├── pyproject.toml
|
|
142
|
+
├── README.md
|
|
143
|
+
├── LICENSE
|
|
144
|
+
├── src/geneflowdecoder/
|
|
145
|
+
│ ├── __init__.py # 公开 API
|
|
146
|
+
│ ├── _core.py # 模型 + fit/forward/predict
|
|
147
|
+
│ └── _version.py
|
|
148
|
+
└── tests/
|
|
149
|
+
└── test_api.py
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# GeneFlowDecoder
|
|
2
|
+
|
|
3
|
+
`z → X` 的细胞状态解码器:**Gene Transformer + Conditional Rectified Flow (CFM)**,
|
|
4
|
+
把固定的细胞表征 `z` 解码成 log1p 归一化表达谱 `X`。
|
|
5
|
+
|
|
6
|
+
> 设计目标:接口极简、可复现、单卡可跑 2 万+ 基因面板;`z` 默认冻结,
|
|
7
|
+
> 不参与反传,也不被优化器更新。
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from geneflowdecoder import GeneFlowDecoder
|
|
11
|
+
|
|
12
|
+
model = GeneFlowDecoder(n_genes=2048, z_dim=256, d=512, depth=8)
|
|
13
|
+
|
|
14
|
+
model.fit(X_train, z_train, # X: [N, G] log1p; z: [N, D]
|
|
15
|
+
X_val=X_val, z_val=z_val, # 可选,开启早停
|
|
16
|
+
epochs=200, batch_size=64)
|
|
17
|
+
|
|
18
|
+
X_hat = model(z_test) # 采样(生成式,log1p 空间)
|
|
19
|
+
X_hat = model(z_test, steps=8, deterministic=False)
|
|
20
|
+
|
|
21
|
+
X_rec = model.predict(z_test) # 确定性还原:NB 均值
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 安装
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# 从源码(开发模式)
|
|
30
|
+
git clone https://github.com/your-org/geneflowdecoder.git
|
|
31
|
+
cd geneflowdecoder
|
|
32
|
+
pip install -e .[dev]
|
|
33
|
+
|
|
34
|
+
# 或构建 wheel 后安装
|
|
35
|
+
python -m build
|
|
36
|
+
pip install dist/geneflowdecoder-0.1.0-py3-none-any.whl
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
依赖:`torch >= 2.1`、`numpy >= 1.24`。没有 GPU 也能在 CPU 上跑(只是慢)。
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
### `GeneFlowDecoder(...)`
|
|
46
|
+
|
|
47
|
+
| 参数 | 默认 | 说明 |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `n_genes` | — | 基因数,即输出维度 `G` |
|
|
50
|
+
| `z_dim` | — | 细胞状态 `z` 的维度 |
|
|
51
|
+
| `d` | 768 | Transformer 宽度 |
|
|
52
|
+
| `depth` | 16 | Transformer 层数 |
|
|
53
|
+
| `n_heads` | 12 | 注意力头数 |
|
|
54
|
+
| `chunk_size` | 2048 | 基因轴分块,控制显存峰值 |
|
|
55
|
+
| `gradient_checkpoint` | `True` | 激活检查点,省显存 |
|
|
56
|
+
| `cfg` | `FlowConfig()` | flow / loss 超参 |
|
|
57
|
+
| `freeze_z` | `True` | **`z` 全程 `detach`,只读条件** |
|
|
58
|
+
| `device` | `"auto"` | `"auto"` / `"cuda"` / `"cpu"` |
|
|
59
|
+
|
|
60
|
+
### `model.fit(X, z, *, X_val=None, z_val=None, epochs=200, batch_size=64, lr=2e-4, weight_decay=0.02, patience=20, num_workers=0, verbose=1) -> self`
|
|
61
|
+
|
|
62
|
+
- `X` / `X_val`:`[N, G]` log1p 表达,numpy 或 torch 都行
|
|
63
|
+
- `z` / `z_val`:`[N, z_dim]`;`freeze_z=True` 时绝不会被修改或反传
|
|
64
|
+
- 给了验证集 → 早停 + 自动回滚最优权重
|
|
65
|
+
|
|
66
|
+
### `model(z, *, steps=4, method="midpoint", deterministic=False) -> Tensor`
|
|
67
|
+
|
|
68
|
+
Flow 采样,`steps=4` 通常够用。
|
|
69
|
+
|
|
70
|
+
### `model.predict(z) -> Tensor`
|
|
71
|
+
|
|
72
|
+
确定性输出 `E[X]` 的 log1p(NB mean)。转伪计数:`(log_mu.exp() - 1).clamp(min=0)`。
|
|
73
|
+
|
|
74
|
+
### `model.save(path)` / `GeneFlowDecoder.load(path, device="auto")`
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## `FlowConfig`
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from geneflowdecoder import FlowConfig
|
|
82
|
+
|
|
83
|
+
cfg = FlowConfig(
|
|
84
|
+
sigma_min=1e-4, # t 的截断,防 t≈0 数值爆炸
|
|
85
|
+
nb_weight=1.0, # NB 似然辅助损失权重
|
|
86
|
+
corr_weight=0.1, # cell-wise Pearson 相关性正则
|
|
87
|
+
)
|
|
88
|
+
model = GeneFlowDecoder(n_genes=G, z_dim=D, cfg=cfg)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 发布
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python -m build # 生成 sdist + wheel 到 dist/
|
|
97
|
+
twine check dist/* # 校验元数据
|
|
98
|
+
twine upload dist/* # 发布到 PyPI(需账号 token)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 目录结构
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
geneflowdecoder/
|
|
107
|
+
├── pyproject.toml
|
|
108
|
+
├── README.md
|
|
109
|
+
├── LICENSE
|
|
110
|
+
├── src/geneflowdecoder/
|
|
111
|
+
│ ├── __init__.py # 公开 API
|
|
112
|
+
│ ├── _core.py # 模型 + fit/forward/predict
|
|
113
|
+
│ └── _version.py
|
|
114
|
+
└── tests/
|
|
115
|
+
└── test_api.py
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "geneflowdecoder"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Gene Transformer + Conditional Rectified Flow decoder from cell-state z to log1p expression X."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
license = {text = "MIT"}
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "GeneFlowDecoder Contributors"}
|
|
10
|
+
]
|
|
11
|
+
keywords = [
|
|
12
|
+
"single-cell", "transcriptomics", "decoder", "flow-matching",
|
|
13
|
+
"gene-transformer", "scvi", "representation-learning",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Bio-Informatics",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"torch >= 2.1",
|
|
29
|
+
"numpy >= 1.24",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
dev = [
|
|
34
|
+
"pytest >= 7",
|
|
35
|
+
"build",
|
|
36
|
+
"twine",
|
|
37
|
+
]
|
|
38
|
+
test = ["pytest >= 7"]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/ZengFLab/geneflowdecoder"
|
|
42
|
+
Documentation = "https://github.com/ZengFLab/geneflowdecoder#readme"
|
|
43
|
+
Repository = "https://github.com/ZengFLab/geneflowdecoder"
|
|
44
|
+
Issues = "https://github.com/ZengFLab/geneflowdecoder/issues"
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["setuptools >= 61.0"]
|
|
48
|
+
build-backend = "setuptools.build_meta"
|
|
49
|
+
|
|
50
|
+
[tool.setuptools.packages.find]
|
|
51
|
+
where = ["src"]
|
|
52
|
+
include = ["geneflowdecoder*"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
addopts = "-q --import-mode=importlib"
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 88
|
|
60
|
+
target-version = "py39"
|
|
61
|
+
|
|
62
|
+
[tool.mypy]
|
|
63
|
+
python_version = "3.9"
|
|
64
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""GeneFlowDecoder: z -> X decoder (Gene Transformer + Rectified Flow)."""
|
|
3
|
+
|
|
4
|
+
from ._core import GeneFlowDecoder, FlowConfig
|
|
5
|
+
from ._version import __version__
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"GeneFlowDecoder",
|
|
9
|
+
"FlowConfig",
|
|
10
|
+
"__version__",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
__doc__ = GeneFlowDecoder.__doc__ or ""
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
GeneFlowDecoder
|
|
4
|
+
===============
|
|
5
|
+
|
|
6
|
+
z (fixed cell-state representation) --> X (log-normalized expression, log1p space)
|
|
7
|
+
|
|
8
|
+
Public API
|
|
9
|
+
----------
|
|
10
|
+
model = GeneFlowDecoder(n_genes=2048, z_dim=256)
|
|
11
|
+
model.fit(X, z) # train
|
|
12
|
+
X_hat = model(z) # sample (generative)
|
|
13
|
+
X_hat = model.predict(z) # deterministic (NB mean)
|
|
14
|
+
|
|
15
|
+
This module is framework-agnostic beyond ``torch`` and ``numpy``.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import math
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Optional, Tuple, Union
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import torch
|
|
28
|
+
import torch.nn as nn
|
|
29
|
+
import torch.nn.functional as F
|
|
30
|
+
_no_grad = torch.no_grad
|
|
31
|
+
except Exception: # pragma: no cover - soft dep for import-only environments
|
|
32
|
+
torch = None # type: ignore
|
|
33
|
+
nn = object # type: ignore
|
|
34
|
+
F = object # type: ignore
|
|
35
|
+
|
|
36
|
+
def _no_grad(fn): # identity decorator when torch is absent
|
|
37
|
+
return fn
|
|
38
|
+
|
|
39
|
+
Tensor = object # placeholder for type annotations only
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
from torch import Tensor as _TorchTensor # noqa: F401
|
|
43
|
+
Tensor = _TorchTensor # type: ignore
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
Array = Union[np.ndarray, torch.Tensor]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _as_tensor(x: Array, device: torch.device) -> torch.Tensor:
|
|
51
|
+
if isinstance(x, np.ndarray):
|
|
52
|
+
x = torch.from_numpy(x)
|
|
53
|
+
return x.to(device, non_blocking=True)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ----------------------------------------------------------------------
|
|
57
|
+
# 1. Conditional modulation & transformer blocks
|
|
58
|
+
# ----------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
class _AdaLNModulation(nn.Module):
|
|
61
|
+
def __init__(self, d: int, cond_dim: int):
|
|
62
|
+
super().__init__()
|
|
63
|
+
self.norm = nn.LayerNorm(d, elementwise_affine=False, eps=1e-6)
|
|
64
|
+
self.proj = nn.Sequential(
|
|
65
|
+
nn.Linear(cond_dim, d), nn.SiLU(), nn.Linear(d, 3 * d)
|
|
66
|
+
)
|
|
67
|
+
nn.init.zeros_(self.proj[-1].weight)
|
|
68
|
+
nn.init.zeros_(self.proj[-1].bias)
|
|
69
|
+
|
|
70
|
+
def forward(self, x: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
|
|
71
|
+
g, b, gate = self.proj(z).chunk(3, dim=-1)
|
|
72
|
+
g, b, gate = g.unsqueeze(1), b.unsqueeze(1), gate.unsqueeze(1).sigmoid()
|
|
73
|
+
return gate * (self.norm(x) * (1 + g) + b)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class _CondBlock(nn.Module):
|
|
77
|
+
def __init__(self, d: int, cond_dim: int, n_heads: int, mlp_ratio: float = 4.0):
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.attn_norm = _AdaLNModulation(d, cond_dim)
|
|
80
|
+
self.attn = nn.MultiheadAttention(d, n_heads, batch_first=True)
|
|
81
|
+
self.mlp_norm = _AdaLNModulation(d, cond_dim)
|
|
82
|
+
h = int(d * mlp_ratio)
|
|
83
|
+
self.mlp = nn.Sequential(nn.Linear(d, h), nn.GELU(), nn.Linear(h, d))
|
|
84
|
+
|
|
85
|
+
def forward(self, x: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
|
|
86
|
+
h = self.attn_norm(x, z)
|
|
87
|
+
h, _ = self.attn(h, h, h, need_weights=False)
|
|
88
|
+
x = x + h
|
|
89
|
+
return x + self.mlp(self.mlp_norm(x, z))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class _GeneTransformer(nn.Module):
|
|
93
|
+
def __init__(
|
|
94
|
+
self,
|
|
95
|
+
n_genes: int,
|
|
96
|
+
d: int,
|
|
97
|
+
depth: int,
|
|
98
|
+
n_heads: int,
|
|
99
|
+
cond_dim: int,
|
|
100
|
+
chunk_size: int,
|
|
101
|
+
ckpt: bool,
|
|
102
|
+
):
|
|
103
|
+
super().__init__()
|
|
104
|
+
self.d = d
|
|
105
|
+
self.n_genes = n_genes
|
|
106
|
+
self.chunk_size = chunk_size
|
|
107
|
+
self.gene_embed = nn.Embedding(n_genes, d)
|
|
108
|
+
nn.init.normal_(self.gene_embed.weight, std=0.02)
|
|
109
|
+
self.pos_embed = nn.Parameter(torch.zeros(1, n_genes, d))
|
|
110
|
+
nn.init.normal_(self.pos_embed, std=0.02)
|
|
111
|
+
|
|
112
|
+
self.blocks = nn.ModuleList(
|
|
113
|
+
[_CondBlock(d, cond_dim, n_heads) for _ in range(depth)]
|
|
114
|
+
)
|
|
115
|
+
self.head_norm = nn.LayerNorm(d, eps=1e-6)
|
|
116
|
+
self._ckpt = ckpt
|
|
117
|
+
|
|
118
|
+
def _run(self, x: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
|
|
119
|
+
for blk in self.blocks:
|
|
120
|
+
if self._ckpt and self.training:
|
|
121
|
+
x = torch.utils.checkpoint.checkpoint(
|
|
122
|
+
blk, x, z, use_reentrant=False
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
x = blk(x, z)
|
|
126
|
+
return self.head_norm(x)
|
|
127
|
+
|
|
128
|
+
def forward(self, gene_ids: torch.Tensor, z: torch.Tensor) -> torch.Tensor:
|
|
129
|
+
B, G = gene_ids.shape[0], gene_ids.shape[-1]
|
|
130
|
+
if G <= self.chunk_size:
|
|
131
|
+
h = self.gene_embed(gene_ids) + self.pos_embed[:, :G, :]
|
|
132
|
+
return self._run(h, z)
|
|
133
|
+
|
|
134
|
+
outs: list[torch.Tensor] = []
|
|
135
|
+
s = 0
|
|
136
|
+
while s < G:
|
|
137
|
+
e = min(s + self.chunk_size, G)
|
|
138
|
+
h = self.gene_embed(gene_ids[..., s:e]) + self.pos_embed[:, s:e, :]
|
|
139
|
+
outs.append(self._run(h, z))
|
|
140
|
+
s = e
|
|
141
|
+
return torch.cat(outs, dim=1)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ----------------------------------------------------------------------
|
|
145
|
+
# 2. Negative-binomial output head
|
|
146
|
+
# ----------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
class _NBHead(nn.Module):
|
|
149
|
+
def __init__(self, d: int, n_genes: int):
|
|
150
|
+
super().__init__()
|
|
151
|
+
self.mu_proj = nn.Linear(d, n_genes)
|
|
152
|
+
self.theta = nn.Parameter(torch.zeros(n_genes))
|
|
153
|
+
nn.init.zeros_(self.mu_proj.weight)
|
|
154
|
+
nn.init.zeros_(self.mu_proj.bias)
|
|
155
|
+
|
|
156
|
+
def forward(self, h: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
157
|
+
return self.mu_proj(h), F.softplus(self.theta) + 1e-3
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def nll(
|
|
161
|
+
log_mu: torch.Tensor,
|
|
162
|
+
theta: torch.Tensor,
|
|
163
|
+
x: torch.Tensor,
|
|
164
|
+
mask: Optional[torch.Tensor] = None,
|
|
165
|
+
) -> torch.Tensor:
|
|
166
|
+
mu = log_mu.exp().clamp(min=1e-6)
|
|
167
|
+
th = theta.unsqueeze(0).expand_as(x)
|
|
168
|
+
ll = (
|
|
169
|
+
torch.lgamma(x + th)
|
|
170
|
+
- torch.lgamma(th)
|
|
171
|
+
- torch.lgamma(x + 1)
|
|
172
|
+
+ th * (th.log() - (th + mu).log())
|
|
173
|
+
+ x * (mu.log() - (th + mu).log())
|
|
174
|
+
)
|
|
175
|
+
if mask is not None:
|
|
176
|
+
ll = ll * mask
|
|
177
|
+
return -ll.sum() / mask.sum().clamp(min=1.0)
|
|
178
|
+
return -ll.mean()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@dataclass
|
|
182
|
+
class FlowConfig:
|
|
183
|
+
"""Hyper-parameters of the conditional flow matching objective."""
|
|
184
|
+
|
|
185
|
+
sigma_min: float = 1e-4
|
|
186
|
+
nb_weight: float = 1.0
|
|
187
|
+
corr_weight: float = 0.1
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ----------------------------------------------------------------------
|
|
191
|
+
# 3. GeneFlowDecoder
|
|
192
|
+
# ----------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
class GeneFlowDecoder(nn.Module):
|
|
195
|
+
"""
|
|
196
|
+
Conditional Rectified-Flow decoder from a **fixed** cell-state vector *z*
|
|
197
|
+
to a log1p expression profile *X*.
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
n_genes : int
|
|
202
|
+
Number of genes (output dimension).
|
|
203
|
+
z_dim : int
|
|
204
|
+
Dimensionality of the cell-state representation.
|
|
205
|
+
d, depth, n_heads : int
|
|
206
|
+
Transformer width, number of layers, number of attention heads.
|
|
207
|
+
chunk_size : int
|
|
208
|
+
Chunk size for the gene axis (enables >10k gene panels on one GPU).
|
|
209
|
+
gradient_checkpoint : bool
|
|
210
|
+
Enable activation checkpointing inside the transformer.
|
|
211
|
+
cfg : FlowConfig | None
|
|
212
|
+
Flow / loss hyper-parameters.
|
|
213
|
+
freeze_z : bool, default ``True``
|
|
214
|
+
When ``True`` (recommended), *z* is detached everywhere: it is treated
|
|
215
|
+
as a fixed, non-trainable conditioning signal and is never updated.
|
|
216
|
+
device : ``"auto"`` | ``str`` | ``torch.device``
|
|
217
|
+
Where to place the model. ``"auto"`` picks CUDA if available.
|
|
218
|
+
|
|
219
|
+
Examples
|
|
220
|
+
--------
|
|
221
|
+
>>> import numpy as np
|
|
222
|
+
>>> from geneflowdecoder import GeneFlowDecoder
|
|
223
|
+
>>> G, D = 2048, 128
|
|
224
|
+
>>> model = GeneFlowDecoder(n_genes=G, z_dim=D, d=512, depth=8)
|
|
225
|
+
>>> X = np.random.randn(1024, G).astype("float32")
|
|
226
|
+
>>> z = np.random.randn(1024, D).astype("float32")
|
|
227
|
+
>>> model.fit(X, z, epochs=10, batch_size=64)
|
|
228
|
+
>>> X_hat = model(z[:4]) # sampled, log1p space
|
|
229
|
+
>>> X_rec = model.predict(z[:4]) # deterministic NB mean
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
def __init__(
|
|
233
|
+
self,
|
|
234
|
+
n_genes: int,
|
|
235
|
+
z_dim: int,
|
|
236
|
+
*,
|
|
237
|
+
d: int = 768,
|
|
238
|
+
depth: int = 16,
|
|
239
|
+
n_heads: int = 12,
|
|
240
|
+
chunk_size: int = 2048,
|
|
241
|
+
gradient_checkpoint: bool = True,
|
|
242
|
+
cfg: Optional[FlowConfig] = None,
|
|
243
|
+
freeze_z: bool = True,
|
|
244
|
+
device: str = "auto",
|
|
245
|
+
):
|
|
246
|
+
super().__init__()
|
|
247
|
+
if device == "auto":
|
|
248
|
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
|
249
|
+
self.device = torch.device(device)
|
|
250
|
+
self.n_genes = int(n_genes)
|
|
251
|
+
self.z_dim = int(z_dim)
|
|
252
|
+
self.freeze_z = bool(freeze_z)
|
|
253
|
+
self._cfg = cfg or FlowConfig()
|
|
254
|
+
|
|
255
|
+
self.backbone = _GeneTransformer(
|
|
256
|
+
n_genes, d, depth, n_heads, z_dim, chunk_size, gradient_checkpoint
|
|
257
|
+
)
|
|
258
|
+
self.nb_head = _NBHead(d, n_genes)
|
|
259
|
+
self.vel_proj = nn.Linear(d, n_genes)
|
|
260
|
+
self.time_mlp = nn.Sequential(
|
|
261
|
+
nn.Linear(128, d), nn.SiLU(), nn.Linear(d, d)
|
|
262
|
+
)
|
|
263
|
+
self._gene_ids = nn.Parameter(
|
|
264
|
+
torch.arange(self.n_genes), requires_grad=False
|
|
265
|
+
)
|
|
266
|
+
self.to(self.device)
|
|
267
|
+
|
|
268
|
+
# ----------------------------------------------------------------
|
|
269
|
+
# internals
|
|
270
|
+
# ----------------------------------------------------------------
|
|
271
|
+
def _sincos(self, t: torch.Tensor) -> torch.Tensor:
|
|
272
|
+
half = 64
|
|
273
|
+
freqs = torch.exp(
|
|
274
|
+
-math.log(10000.0) * torch.arange(half, device=t.device) / (half - 1)
|
|
275
|
+
)
|
|
276
|
+
a = t[:, None] * freqs[None, :]
|
|
277
|
+
e = torch.cat([a.sin(), a.cos()], dim=-1)
|
|
278
|
+
if e.shape[-1] < 128:
|
|
279
|
+
e = F.pad(e, (0, 128 - e.shape[-1]))
|
|
280
|
+
return e
|
|
281
|
+
|
|
282
|
+
def _maybe_detach(
|
|
283
|
+
self, z: torch.Tensor, *, snapshot: bool = False
|
|
284
|
+
) -> torch.Tensor:
|
|
285
|
+
"""冻结 z 时截断计算图;``snapshot=True`` 再额外拷一份独立副本。
|
|
286
|
+
|
|
287
|
+
snapshot 只在 fit 入口用:把当轮 z 固化下来,之后外部 encoder
|
|
288
|
+
若继续 backward / 原地更新,训练集 z 的数值也不会跟着变。
|
|
289
|
+
单次推理(forward / predict)不开 snapshot,省一次显存拷贝。
|
|
290
|
+
"""
|
|
291
|
+
if not self.freeze_z:
|
|
292
|
+
return z
|
|
293
|
+
z = z.detach()
|
|
294
|
+
return z.clone() if snapshot else z
|
|
295
|
+
|
|
296
|
+
def _velocity(
|
|
297
|
+
self, x_t: torch.Tensor, t: torch.Tensor, z: torch.Tensor
|
|
298
|
+
) -> torch.Tensor:
|
|
299
|
+
z = self._maybe_detach(z)
|
|
300
|
+
B = x_t.shape[0]
|
|
301
|
+
t_emb = self.time_mlp(self._sincos(t)).unsqueeze(1)
|
|
302
|
+
gids = self._gene_ids.unsqueeze(0).expand(B, -1)
|
|
303
|
+
h = self.backbone(gids, z) + t_emb
|
|
304
|
+
return self.vel_proj(h)
|
|
305
|
+
|
|
306
|
+
# ----------------------------------------------------------------
|
|
307
|
+
# objective
|
|
308
|
+
# ----------------------------------------------------------------
|
|
309
|
+
def _loss(
|
|
310
|
+
self,
|
|
311
|
+
x: torch.Tensor,
|
|
312
|
+
z: torch.Tensor,
|
|
313
|
+
mask: Optional[torch.Tensor] = None,
|
|
314
|
+
) -> torch.Tensor:
|
|
315
|
+
z = self._maybe_detach(z)
|
|
316
|
+
B = x.shape[0]
|
|
317
|
+
t = torch.rand(B, device=self.device).clamp(
|
|
318
|
+
self._cfg.sigma_min, 1.0 - self._cfg.sigma_min
|
|
319
|
+
)
|
|
320
|
+
eps = torch.randn_like(x)
|
|
321
|
+
x_t = x.lerp(eps, t.unsqueeze(1))
|
|
322
|
+
|
|
323
|
+
v_pred = self._velocity(x_t, t, z)
|
|
324
|
+
v_target = eps - x # rectified-flow target
|
|
325
|
+
mse = F.mse_loss(v_pred, v_target, reduction="none")
|
|
326
|
+
if mask is not None:
|
|
327
|
+
mse = mse * mask
|
|
328
|
+
mse = mse.sum() / mask.sum().clamp(min=1.0)
|
|
329
|
+
else:
|
|
330
|
+
mse = mse.mean()
|
|
331
|
+
|
|
332
|
+
x0_hat = x_t.lerp(v_pred.detach(), t.unsqueeze(1))
|
|
333
|
+
|
|
334
|
+
nb = torch.tensor(0.0, device=self.device)
|
|
335
|
+
if self._cfg.nb_weight > 0:
|
|
336
|
+
gids = self._gene_ids.unsqueeze(0).expand(B, -1)
|
|
337
|
+
log_mu, theta = self.nb_head(self.backbone(gids, z))
|
|
338
|
+
nb = self.nb_head.nll(log_mu, theta, x, mask)
|
|
339
|
+
|
|
340
|
+
corr = torch.tensor(0.0, device=self.device)
|
|
341
|
+
if self._cfg.corr_weight > 0 and mask is None:
|
|
342
|
+
corr = self._corr(x0_hat, x)
|
|
343
|
+
|
|
344
|
+
return (
|
|
345
|
+
mse
|
|
346
|
+
+ self._cfg.nb_weight * nb
|
|
347
|
+
+ self._cfg.corr_weight * corr
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
@staticmethod
|
|
351
|
+
def _corr(p: torch.Tensor, q: torch.Tensor) -> torch.Tensor:
|
|
352
|
+
p = p - p.mean(1, keepdim=True)
|
|
353
|
+
q = q - q.mean(1, keepdim=True)
|
|
354
|
+
denom = p.norm(dim=1) * q.norm(dim=1) + 1e-8
|
|
355
|
+
return (1 - (p * q).sum(dim=1) / denom).mean()
|
|
356
|
+
|
|
357
|
+
# ----------------------------------------------------------------
|
|
358
|
+
# fit
|
|
359
|
+
# ----------------------------------------------------------------
|
|
360
|
+
def fit(
|
|
361
|
+
self,
|
|
362
|
+
X: Array,
|
|
363
|
+
z: Array,
|
|
364
|
+
*,
|
|
365
|
+
X_val: Optional[Array] = None,
|
|
366
|
+
z_val: Optional[Array] = None,
|
|
367
|
+
epochs: int = 200,
|
|
368
|
+
batch_size: int = 64,
|
|
369
|
+
lr: float = 2e-4,
|
|
370
|
+
weight_decay: float = 0.02,
|
|
371
|
+
patience: int = 20,
|
|
372
|
+
num_workers: int = 0,
|
|
373
|
+
verbose: int = 1,
|
|
374
|
+
) -> "GeneFlowDecoder":
|
|
375
|
+
"""
|
|
376
|
+
Train the decoder.
|
|
377
|
+
|
|
378
|
+
Parameters
|
|
379
|
+
----------
|
|
380
|
+
X : array, shape ``(N, n_genes)``
|
|
381
|
+
Log1p-normalized expression matrix.
|
|
382
|
+
z : array, shape ``(N, z_dim)``
|
|
383
|
+
Cell-state vectors. **Never updated** when ``freeze_z=True``.
|
|
384
|
+
X_val, z_val : arrays, optional
|
|
385
|
+
Validation split. If given, early stopping is enabled and the
|
|
386
|
+
best weights (lowest validation loss) are restored at the end.
|
|
387
|
+
epochs, batch_size, lr, weight_decay, patience
|
|
388
|
+
Standard optimisation knobs.
|
|
389
|
+
num_workers : int
|
|
390
|
+
Workers for the training DataLoader.
|
|
391
|
+
verbose : int
|
|
392
|
+
0 = silent, 1 = epoch logs.
|
|
393
|
+
|
|
394
|
+
Returns
|
|
395
|
+
-------
|
|
396
|
+
self
|
|
397
|
+
"""
|
|
398
|
+
X = _as_tensor(X, self.device)
|
|
399
|
+
z = self._maybe_detach(_as_tensor(z, self.device), snapshot=True)
|
|
400
|
+
|
|
401
|
+
ds = torch.utils.data.TensorDataset(X, z)
|
|
402
|
+
dl = torch.utils.data.DataLoader(
|
|
403
|
+
ds,
|
|
404
|
+
batch_size=int(batch_size),
|
|
405
|
+
shuffle=True,
|
|
406
|
+
num_workers=int(num_workers),
|
|
407
|
+
pin_memory=True,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
has_val = False
|
|
411
|
+
if X_val is not None and z_val is not None:
|
|
412
|
+
Xv = _as_tensor(X_val, self.device)
|
|
413
|
+
zv = self._maybe_detach(_as_tensor(z_val, self.device), snapshot=True)
|
|
414
|
+
has_val = True
|
|
415
|
+
|
|
416
|
+
# Only decoder-owned parameters are optimised.
|
|
417
|
+
params = [p for p in self.parameters() if p.requires_grad]
|
|
418
|
+
opt = torch.optim.AdamW(params, lr=lr, weight_decay=weight_decay)
|
|
419
|
+
sched = torch.optim.lr_scheduler.CosineAnnealingLR(
|
|
420
|
+
opt, T_max=int(epochs)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
best_val = float("inf")
|
|
424
|
+
best_state: Optional[dict[str, torch.Tensor]] = None
|
|
425
|
+
no_improve = 0
|
|
426
|
+
|
|
427
|
+
for ep in range(1, int(epochs) + 1):
|
|
428
|
+
self.train()
|
|
429
|
+
tot = 0.0
|
|
430
|
+
for xb, zb in dl:
|
|
431
|
+
opt.zero_grad()
|
|
432
|
+
loss = self._loss(xb, zb)
|
|
433
|
+
loss.backward()
|
|
434
|
+
nn.utils.clip_grad_norm_(params, 1.0)
|
|
435
|
+
opt.step()
|
|
436
|
+
tot += loss.item() * xb.shape[0]
|
|
437
|
+
sched.step()
|
|
438
|
+
|
|
439
|
+
if verbose:
|
|
440
|
+
msg = (
|
|
441
|
+
f"[fit] epoch {ep:3d}/{epochs} "
|
|
442
|
+
f"| train={tot / len(ds):.4f}"
|
|
443
|
+
)
|
|
444
|
+
if has_val:
|
|
445
|
+
self.eval()
|
|
446
|
+
with torch.no_grad():
|
|
447
|
+
vl = self._loss(Xv, zv).item()
|
|
448
|
+
msg += f" | val={vl:.4f}"
|
|
449
|
+
if vl < best_val - 1e-5:
|
|
450
|
+
best_val, no_improve = vl, 0
|
|
451
|
+
best_state = {
|
|
452
|
+
k: v.detach().cpu().clone()
|
|
453
|
+
for k, v in self.state_dict().items()
|
|
454
|
+
}
|
|
455
|
+
else:
|
|
456
|
+
no_improve += 1
|
|
457
|
+
if no_improve >= int(patience):
|
|
458
|
+
if verbose:
|
|
459
|
+
print(f"[fit] early stop at epoch {ep}")
|
|
460
|
+
break
|
|
461
|
+
print(msg)
|
|
462
|
+
|
|
463
|
+
if best_state is not None:
|
|
464
|
+
self.load_state_dict(best_state)
|
|
465
|
+
if verbose:
|
|
466
|
+
print(f"[fit] restored best val={best_val:.4f}")
|
|
467
|
+
return self
|
|
468
|
+
|
|
469
|
+
# ----------------------------------------------------------------
|
|
470
|
+
# inference
|
|
471
|
+
# ----------------------------------------------------------------
|
|
472
|
+
@torch.no_grad()
|
|
473
|
+
def forward(
|
|
474
|
+
self,
|
|
475
|
+
z: Array,
|
|
476
|
+
*,
|
|
477
|
+
steps: int = 4,
|
|
478
|
+
method: str = "midpoint",
|
|
479
|
+
deterministic: bool = False,
|
|
480
|
+
) -> torch.Tensor:
|
|
481
|
+
"""
|
|
482
|
+
Sample an expression profile from *z* (log1p space).
|
|
483
|
+
|
|
484
|
+
Parameters
|
|
485
|
+
----------
|
|
486
|
+
z : array, shape ``(B, z_dim)``
|
|
487
|
+
steps : int
|
|
488
|
+
Number of flow-integration steps (4 is usually enough).
|
|
489
|
+
method : ``"midpoint"`` | ``"euler"``
|
|
490
|
+
deterministic : bool
|
|
491
|
+
If ``True``, returns the deterministic NB mean
|
|
492
|
+
(equivalent to ``self.predict(z)``).
|
|
493
|
+
"""
|
|
494
|
+
z = self._maybe_detach(_as_tensor(z, self.device))
|
|
495
|
+
if deterministic:
|
|
496
|
+
return self.predict(z)
|
|
497
|
+
|
|
498
|
+
self.eval()
|
|
499
|
+
B = z.shape[0]
|
|
500
|
+
x = torch.randn(B, self.n_genes, device=self.device)
|
|
501
|
+
ts = torch.linspace(0.0, 1.0, int(steps) + 1, device=self.device)
|
|
502
|
+
for i in range(int(steps)):
|
|
503
|
+
t0, t1 = ts[i], ts[i + 1]
|
|
504
|
+
tmid = (t0 + t1) / 2
|
|
505
|
+
dt = (t1 - t0).item()
|
|
506
|
+
v = self._velocity(x, torch.full((B,), t0, device=self.device), z)
|
|
507
|
+
if method == "midpoint":
|
|
508
|
+
xm = x + 0.5 * dt * v
|
|
509
|
+
v = self._velocity(
|
|
510
|
+
xm, torch.full((B,), tmid, device=self.device), z
|
|
511
|
+
)
|
|
512
|
+
x = x + dt * v
|
|
513
|
+
return x
|
|
514
|
+
|
|
515
|
+
@torch.no_grad()
|
|
516
|
+
def predict(self, z: Array) -> torch.Tensor:
|
|
517
|
+
"""
|
|
518
|
+
Deterministic reconstruction: the NB mean in log1p space.
|
|
519
|
+
|
|
520
|
+
Convert to pseudo-counts with ``(log_mu.exp() - 1).clamp(min=0)``.
|
|
521
|
+
"""
|
|
522
|
+
z = self._maybe_detach(_as_tensor(z, self.device))
|
|
523
|
+
self.eval()
|
|
524
|
+
B = z.shape[0]
|
|
525
|
+
gids = self._gene_ids.unsqueeze(0).expand(B, -1)
|
|
526
|
+
log_mu, _ = self.nb_head(self.backbone(gids, z))
|
|
527
|
+
return log_mu
|
|
528
|
+
|
|
529
|
+
# ----------------------------------------------------------------
|
|
530
|
+
# persistence
|
|
531
|
+
# ----------------------------------------------------------------
|
|
532
|
+
def save(self, path: str) -> None:
|
|
533
|
+
"""Save model weights + config to ``path``."""
|
|
534
|
+
torch.save(
|
|
535
|
+
{
|
|
536
|
+
"state": self.state_dict(),
|
|
537
|
+
"cfg": self._cfg,
|
|
538
|
+
"freeze_z": self.freeze_z,
|
|
539
|
+
"n_genes": self.n_genes,
|
|
540
|
+
"z_dim": self.z_dim,
|
|
541
|
+
},
|
|
542
|
+
path,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
@classmethod
|
|
546
|
+
def load(
|
|
547
|
+
cls,
|
|
548
|
+
path: str,
|
|
549
|
+
*,
|
|
550
|
+
device: str = "auto",
|
|
551
|
+
**init_kwargs,
|
|
552
|
+
) -> "GeneFlowDecoder":
|
|
553
|
+
"""
|
|
554
|
+
Load a saved model.
|
|
555
|
+
|
|
556
|
+
``n_genes`` / ``z_dim`` / ``freeze_z`` are read from the checkpoint
|
|
557
|
+
unless explicitly overridden in ``init_kwargs``.
|
|
558
|
+
"""
|
|
559
|
+
ckpt = torch.load(path, map_location="cpu")
|
|
560
|
+
init_kwargs.setdefault("n_genes", ckpt.get("n_genes"))
|
|
561
|
+
init_kwargs.setdefault("z_dim", ckpt.get("z_dim"))
|
|
562
|
+
init_kwargs.setdefault("freeze_z", ckpt.get("freeze_z", True))
|
|
563
|
+
init_kwargs["device"] = device
|
|
564
|
+
model = cls(**init_kwargs)
|
|
565
|
+
model._cfg = ckpt.get("cfg", model._cfg)
|
|
566
|
+
model.load_state_dict(ckpt["state"])
|
|
567
|
+
return model
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geneflowdecoder
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Gene Transformer + Conditional Rectified Flow decoder from cell-state z to log1p expression X.
|
|
5
|
+
Author: GeneFlowDecoder Contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/ZengFLab/geneflowdecoder
|
|
8
|
+
Project-URL: Documentation, https://github.com/ZengFLab/geneflowdecoder#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/ZengFLab/geneflowdecoder
|
|
10
|
+
Project-URL: Issues, https://github.com/ZengFLab/geneflowdecoder/issues
|
|
11
|
+
Keywords: single-cell,transcriptomics,decoder,flow-matching,gene-transformer,scvi,representation-learning
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: torch>=2.1
|
|
26
|
+
Requires-Dist: numpy>=1.24
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
29
|
+
Requires-Dist: build; extra == "dev"
|
|
30
|
+
Requires-Dist: twine; extra == "dev"
|
|
31
|
+
Provides-Extra: test
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# GeneFlowDecoder
|
|
36
|
+
|
|
37
|
+
`z → X` 的细胞状态解码器:**Gene Transformer + Conditional Rectified Flow (CFM)**,
|
|
38
|
+
把固定的细胞表征 `z` 解码成 log1p 归一化表达谱 `X`。
|
|
39
|
+
|
|
40
|
+
> 设计目标:接口极简、可复现、单卡可跑 2 万+ 基因面板;`z` 默认冻结,
|
|
41
|
+
> 不参与反传,也不被优化器更新。
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from geneflowdecoder import GeneFlowDecoder
|
|
45
|
+
|
|
46
|
+
model = GeneFlowDecoder(n_genes=2048, z_dim=256, d=512, depth=8)
|
|
47
|
+
|
|
48
|
+
model.fit(X_train, z_train, # X: [N, G] log1p; z: [N, D]
|
|
49
|
+
X_val=X_val, z_val=z_val, # 可选,开启早停
|
|
50
|
+
epochs=200, batch_size=64)
|
|
51
|
+
|
|
52
|
+
X_hat = model(z_test) # 采样(生成式,log1p 空间)
|
|
53
|
+
X_hat = model(z_test, steps=8, deterministic=False)
|
|
54
|
+
|
|
55
|
+
X_rec = model.predict(z_test) # 确定性还原:NB 均值
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 安装
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# 从源码(开发模式)
|
|
64
|
+
git clone https://github.com/your-org/geneflowdecoder.git
|
|
65
|
+
cd geneflowdecoder
|
|
66
|
+
pip install -e .[dev]
|
|
67
|
+
|
|
68
|
+
# 或构建 wheel 后安装
|
|
69
|
+
python -m build
|
|
70
|
+
pip install dist/geneflowdecoder-0.1.0-py3-none-any.whl
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
依赖:`torch >= 2.1`、`numpy >= 1.24`。没有 GPU 也能在 CPU 上跑(只是慢)。
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## API
|
|
78
|
+
|
|
79
|
+
### `GeneFlowDecoder(...)`
|
|
80
|
+
|
|
81
|
+
| 参数 | 默认 | 说明 |
|
|
82
|
+
|---|---|---|
|
|
83
|
+
| `n_genes` | — | 基因数,即输出维度 `G` |
|
|
84
|
+
| `z_dim` | — | 细胞状态 `z` 的维度 |
|
|
85
|
+
| `d` | 768 | Transformer 宽度 |
|
|
86
|
+
| `depth` | 16 | Transformer 层数 |
|
|
87
|
+
| `n_heads` | 12 | 注意力头数 |
|
|
88
|
+
| `chunk_size` | 2048 | 基因轴分块,控制显存峰值 |
|
|
89
|
+
| `gradient_checkpoint` | `True` | 激活检查点,省显存 |
|
|
90
|
+
| `cfg` | `FlowConfig()` | flow / loss 超参 |
|
|
91
|
+
| `freeze_z` | `True` | **`z` 全程 `detach`,只读条件** |
|
|
92
|
+
| `device` | `"auto"` | `"auto"` / `"cuda"` / `"cpu"` |
|
|
93
|
+
|
|
94
|
+
### `model.fit(X, z, *, X_val=None, z_val=None, epochs=200, batch_size=64, lr=2e-4, weight_decay=0.02, patience=20, num_workers=0, verbose=1) -> self`
|
|
95
|
+
|
|
96
|
+
- `X` / `X_val`:`[N, G]` log1p 表达,numpy 或 torch 都行
|
|
97
|
+
- `z` / `z_val`:`[N, z_dim]`;`freeze_z=True` 时绝不会被修改或反传
|
|
98
|
+
- 给了验证集 → 早停 + 自动回滚最优权重
|
|
99
|
+
|
|
100
|
+
### `model(z, *, steps=4, method="midpoint", deterministic=False) -> Tensor`
|
|
101
|
+
|
|
102
|
+
Flow 采样,`steps=4` 通常够用。
|
|
103
|
+
|
|
104
|
+
### `model.predict(z) -> Tensor`
|
|
105
|
+
|
|
106
|
+
确定性输出 `E[X]` 的 log1p(NB mean)。转伪计数:`(log_mu.exp() - 1).clamp(min=0)`。
|
|
107
|
+
|
|
108
|
+
### `model.save(path)` / `GeneFlowDecoder.load(path, device="auto")`
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## `FlowConfig`
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from geneflowdecoder import FlowConfig
|
|
116
|
+
|
|
117
|
+
cfg = FlowConfig(
|
|
118
|
+
sigma_min=1e-4, # t 的截断,防 t≈0 数值爆炸
|
|
119
|
+
nb_weight=1.0, # NB 似然辅助损失权重
|
|
120
|
+
corr_weight=0.1, # cell-wise Pearson 相关性正则
|
|
121
|
+
)
|
|
122
|
+
model = GeneFlowDecoder(n_genes=G, z_dim=D, cfg=cfg)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 发布
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
python -m build # 生成 sdist + wheel 到 dist/
|
|
131
|
+
twine check dist/* # 校验元数据
|
|
132
|
+
twine upload dist/* # 发布到 PyPI(需账号 token)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## 目录结构
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
geneflowdecoder/
|
|
141
|
+
├── pyproject.toml
|
|
142
|
+
├── README.md
|
|
143
|
+
├── LICENSE
|
|
144
|
+
├── src/geneflowdecoder/
|
|
145
|
+
│ ├── __init__.py # 公开 API
|
|
146
|
+
│ ├── _core.py # 模型 + fit/forward/predict
|
|
147
|
+
│ └── _version.py
|
|
148
|
+
└── tests/
|
|
149
|
+
└── test_api.py
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/geneflowdecoder/__init__.py
|
|
5
|
+
src/geneflowdecoder/_core.py
|
|
6
|
+
src/geneflowdecoder/_version.py
|
|
7
|
+
src/geneflowdecoder.egg-info/PKG-INFO
|
|
8
|
+
src/geneflowdecoder.egg-info/SOURCES.txt
|
|
9
|
+
src/geneflowdecoder.egg-info/dependency_links.txt
|
|
10
|
+
src/geneflowdecoder.egg-info/requires.txt
|
|
11
|
+
src/geneflowdecoder.egg-info/top_level.txt
|
|
12
|
+
tests/test_api.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
geneflowdecoder
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Smoke tests for the public API of geneflowdecoder."""
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
torch = pytest.importorskip("torch", reason="torch is an optional test dependency")
|
|
8
|
+
|
|
9
|
+
from geneflowdecoder import GeneFlowDecoder, FlowConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _make(n_genes: int = 256, n_cells: int = 32, z_dim: int = 32):
|
|
13
|
+
rng = np.random.default_rng(0)
|
|
14
|
+
X = rng.normal(0, 1, (n_cells, n_genes)).astype(np.float32)
|
|
15
|
+
z = rng.normal(0, 1, (n_cells, z_dim)).astype(np.float32)
|
|
16
|
+
return X, z
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_import_and_init():
|
|
20
|
+
model = GeneFlowDecoder(n_genes=128, z_dim=16, d=64, depth=2, freeze_z=True)
|
|
21
|
+
assert model.n_genes == 128
|
|
22
|
+
assert model.z_dim == 16
|
|
23
|
+
assert model.freeze_z is True
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_fit_runs_and_z_unchanged():
|
|
27
|
+
X, z = _make()
|
|
28
|
+
z_before = z.copy()
|
|
29
|
+
|
|
30
|
+
model = GeneFlowDecoder(
|
|
31
|
+
n_genes=X.shape[1], z_dim=z.shape[1], d=64, depth=2,
|
|
32
|
+
freeze_z=True, device="cpu",
|
|
33
|
+
)
|
|
34
|
+
model.fit(X, z, epochs=3, batch_size=16, verbose=0)
|
|
35
|
+
|
|
36
|
+
# z is not a model parameter and must be numerically untouched
|
|
37
|
+
assert np.array_equal(z, z_before)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_forward_and_predict_shapes():
|
|
41
|
+
X, z = _make(n_genes=256, n_cells=16, z_dim=32)
|
|
42
|
+
model = GeneFlowDecoder(
|
|
43
|
+
n_genes=X.shape[1], z_dim=z.shape[1], d=64, depth=2,
|
|
44
|
+
freeze_z=True, device="cpu",
|
|
45
|
+
)
|
|
46
|
+
model.fit(X, z, epochs=3, batch_size=8, verbose=0)
|
|
47
|
+
|
|
48
|
+
samp = model(z[:5])
|
|
49
|
+
assert samp.shape == (5, X.shape[1])
|
|
50
|
+
assert torch.isfinite(samp).all()
|
|
51
|
+
|
|
52
|
+
rec = model.predict(z[:5])
|
|
53
|
+
assert rec.shape == (5, X.shape[1])
|
|
54
|
+
assert torch.isfinite(rec).all()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_forward_deterministic_flag():
|
|
58
|
+
X, z = _make(n_genes=128, n_cells=8, z_dim=16)
|
|
59
|
+
model = GeneFlowDecoder(
|
|
60
|
+
n_genes=X.shape[1], z_dim=z.shape[1], d=64, depth=2,
|
|
61
|
+
freeze_z=True, device="cpu",
|
|
62
|
+
)
|
|
63
|
+
model.fit(X, z, epochs=2, batch_size=8, verbose=0)
|
|
64
|
+
|
|
65
|
+
a = model(z, deterministic=True)
|
|
66
|
+
b = model.predict(z)
|
|
67
|
+
assert torch.allclose(a, b)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_save_load_roundtrip(tmp_path):
|
|
71
|
+
X, z = _make(n_genes=128, n_cells=8, z_dim=16)
|
|
72
|
+
model = GeneFlowDecoder(
|
|
73
|
+
n_genes=X.shape[1], z_dim=z.shape[1], d=64, depth=2,
|
|
74
|
+
freeze_z=True, device="cpu",
|
|
75
|
+
)
|
|
76
|
+
model.fit(X, z, epochs=2, batch_size=8, verbose=0)
|
|
77
|
+
|
|
78
|
+
p = tmp_path / "model.pt"
|
|
79
|
+
model.save(str(p))
|
|
80
|
+
|
|
81
|
+
loaded = GeneFlowDecoder.load(str(p), device="cpu")
|
|
82
|
+
assert loaded.n_genes == model.n_genes
|
|
83
|
+
assert loaded.freeze_z == model.freeze_z
|
|
84
|
+
|
|
85
|
+
a = model.predict(z)
|
|
86
|
+
b = loaded.predict(z)
|
|
87
|
+
assert torch.allclose(a, b, atol=1e-6)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_flow_config_passed_through():
|
|
91
|
+
cfg = FlowConfig(nb_weight=2.0, corr_weight=0.0, sigma_min=1e-5)
|
|
92
|
+
model = GeneFlowDecoder(
|
|
93
|
+
n_genes=64, z_dim=8, d=32, depth=1, cfg=cfg, device="cpu",
|
|
94
|
+
)
|
|
95
|
+
assert model._cfg.nb_weight == 2.0
|
|
96
|
+
assert model._cfg.sigma_min == 1e-5
|